Compare commits

...

191 Commits

Author SHA1 Message Date
Eng Zer Jun c93618856a
This closes #2029, use a faster deepcopy library (#2030)
Signed-off-by: Eng Zer Jun <engzerjun@gmail.com>
2024-11-22 21:56:38 +08:00
xuri 5f446f25f0
This closes #2025, support set chart axis text direction and rotation
- Add new field Alignment in the ChartAxis data type
- Update unit tests
- Update doc of the AddHeaderFooterImage function
2024-11-15 22:03:10 +08:00
Ilia Mirkin 30d3561d0e
Rename SetLegacyDrawingHF to AddHeaderFooterImage (#2023)
- Add new exported HeaderFooterImagePositionType enumeration
- An error will be return if the image format is unsupported
- Rename exported data type HeaderFooterGraphics to HeaderFooterImageOptions
- Support add and update exist header and footer images
- Changes the VML data ID to sheet ID
- Update unit tests
- Update dependencies modules
2024-11-09 18:36:42 +08:00
Ilia Mirkin d2be5cdf8e
The SetPageLayout function support set page order of page layout (#2022)
- Add new fields PageOrder for PageLayoutOptions
- Add a new exported error variable ErrPageSetupAdjustTo
- An error will be return if the option value of the SetPageLayout function is invalid
- Updated unit tests
2024-11-08 16:59:07 +08:00
Ilia Mirkin b7375bc6d4
This closes #1395, add new function SetLegacyDrawingHF support to set graphics in a header/footer (#2018) 2024-11-04 10:39:55 +08:00
xuri 0d5d1c53b2
This closes #2015, fix a v2.9.0 regression bug introduced by commit 7715c1462a
- Fix corrupted workbook generated by open the workbook generated by stream writer
- Update unit tests
2024-10-25 08:52:59 +08:00
xuri af190c7fdc
This closes #2014, fix redundant none type pattern fill generated
- Simplify unit tests
2024-10-23 22:07:25 +08:00
wushiling50 d1937a0cde
This closes #1885, add new CultureNameJaJP, CultureNameKoKR and CultureNameZhTW enumeration values (#1895)
- Support apply number format for the Japanese calendar years, the Korean Danki calendar and the Republic of China year
- Update unit tests

Signed-off-by: wushiling50 <2531010934@qq.com>
2024-10-21 09:36:04 +08:00
xuri f1d1a5dc2b
This closes #2004, support apply number format for time and duration cell value
- Add unit tests
- Update dependencies modules
2024-10-10 22:44:38 +08:00
Jian Yu, Chen b23e5a26df
This closes #1076, add new function MoveSheet to support changing sheet order in the workbook (#1996)
- Add unit tests
2024-09-30 21:00:59 +08:00
liuwangchao bebb802069
This closes #1999, fix error on GetCellRichText function when read cell without SST index (#2000)
- Add unit test for get cell rich text when string item index is invalid
- Add comments for the variable ErrPasswordLengthInvalid
- Update dependencies modules
- Update GitHub Actions workflow configuration, test on Go 1.23.x
2024-09-27 14:58:18 +08:00
xuri 41c7dd30ce
This closes #1993, support to set and get pivot table classic layout
- Add new field `ClassicLayout` in the `PivotTableOptions`
- Add a new exported error variable `ErrPivotTableClassicLayout`
- Update unit tests
- Add documentation for the SetDefinedName function, ref #1015
2024-09-21 15:39:36 +08:00
ArcholSevier 02189fb016
Ref #65, new formula function DOLLAR (#1992)
- Update unit tests
2024-09-12 22:07:18 +08:00
xuri ad8541790d
This closes #1989, fix incorrect result of formula functions XIRR and XNPV
- Require number data type instead of string
- Fix incorrect formula error type
2024-09-08 12:19:58 +08:00
xuri 8c0ef7f90d
This closes #1983, support create combo chart with same types
- Add new exported ChartLineUnset enumeration
- Fix set line type of line chart does not work
- Support set line type of scatter chart
2024-09-05 21:38:19 +08:00
Ben Smith aca04ecf57
This closes #1987, support absolute paths for pictures (#1988) 2024-09-04 19:47:02 +08:00
Zhang Zhipeng 0447cb22c8
This close #1985, fix incorrect result of cell value after apply general number format (#1986)
- Update unit tests
2024-09-02 09:19:50 +08:00
xuri 29366fd126
Add new fields for pivot table options and pivot field options
- Add new fields FieldPrintTitles and ItemPrintTitles in the PivotTableOptions data type
- Add new fields ShowAll and InsertBlankRow in the PivotTableField data type
- Export 4 constants ExtURIDataField, ExtURIPivotField, ExtURIPivotFilter and ExtURIPivotHierarchy
- Update unit tests
- Update dependencies modules
2024-09-01 12:10:01 +08:00
centurion-hub 8f87131608
This closes #1979, fix decimal value round issue (#1984)
- Updated unit tests
2024-08-31 10:57:26 +08:00
wanghaochen2024 9c460ffe6c
Add support for applying number format expression with language/location tags and ID (#1951)
- Update unit tests for specified date and time format code
2024-08-27 22:41:32 +08:00
zhangyimingdatiancai c805be1f6f
This related for #810, add new functions DeleteSlicer and GetSlicers (#1943)
- Update unit tests
2024-08-23 10:47:47 +08:00
xuri 9a38657515
This related for #720 and closes #1965, add new NumFmt field in the PivotTableField data type
- Support set and get built-in number format of the pivot table data filed cells
- Update unit tests
- Fixed ineffectual assignment issue
2024-08-18 00:18:02 +08:00
xuri d21b598235
This closes #1968, closes #1969
- Fix missing conditional formatting after remove column
- Fix the SetSheetVisible function panic on none views sheet
- Updated unit tests
2024-07-31 09:10:05 +08:00
xuri 30c4cd70e0
This close #1963, prevent the GetStyle function panic when theme without sysClr 2024-07-23 21:48:17 +08:00
xuri d81b4c8661
This closes #1957, fix missing shape macro missing after adjusted drawing object 2024-07-19 22:22:47 +08:00
xuri 4dd34477f7
This closes #1955, refs #119, support to set cell value with an IEEE 754 "not-a-number" value or infinity 2024-07-18 21:05:36 +08:00
pjh591029530 68a1704900
This fix missing horizontal axis in scatter chart with negative values (#1953)
Co-authored-by: Simmons <1815481@qq.com>
2024-07-17 08:44:16 +08:00
xuri 9c278365f2
This closes #1945, an error will be return if column header cell is empty in pivot table data range
- Update unit tests
2024-07-13 10:41:57 +08:00
wxy 307e533061
This closes #1942, fix percent sign missing in formatted result for zero numeric cell value (#1947)
- Updated unit tests
2024-07-12 08:07:19 +08:00
Patrick Wang 431c31029e
This closes #1944, add new TickLabelPosition field in the ChartAxis data type (#1946)
- Introduce new exported ChartTickLabelPositionType enumeration
- Update unit tests
2024-07-11 14:39:16 +08:00
xuri 53b65150ce
This closes #1940, SetCellHyperLink function now support remove hyperlink by None linkType
- Update unit tests
2024-07-07 17:22:13 +08:00
ShowerBandV 7999a492a4
This closes #1937, fix GetPivotTables returns incorrect data range (#1941)
- Add unit test for get pivot table with across worksheet data range, update dependencies package and updated comments of the GetMergeCells function
2024-07-06 09:25:09 +08:00
Aybek b18b48099b
Optimize ColumnNumberToName function performance, reduce about 50% memory usage and 50% time cost (#1935)
Co-authored-by: zhayt <zaibek@wtotem.com>
2024-07-05 14:34:02 +08:00
Vovka Morkovka 4e6457accd
This closes #1926, fix secondary vertical axis title is not displayed (#1928) 2024-06-21 08:13:12 +08:00
wangsongyan f04aa8dd31
Add new AutoFitIgnoreAspect field in the GraphicOptions data type (#1923)
- Support fill the cell with the image and ignore its aspect ratio
- Update the unit tests
2024-06-19 20:45:25 +08:00
联盟少侠 1a99dd4a23
This closes #1921, fix set axis format doesn't work in combo chart (#1924)
- Fix incorrect primary axis titles position
2024-06-17 21:54:15 +08:00
xuri c349313850
This closes #1910, fix a v2.8.1 regression bug introduced by commit 866f3086cd
- Fix spark lines duplicate when creating spark lines on multiple sheets
2024-05-29 21:05:34 +08:00
xiaokui 08d25006f9
This fixed can not found code coverage on Windows (#1908)
Co-authored-by: Qi Jinkui <qijinkui@kkguan.com>
2024-05-27 20:45:37 +08:00
xuri 0c3dfb1605
This closes #1903, made GetCellStyle function concurrency safe
- Update comment of the function and unit test
2024-05-26 01:25:49 +08:00
xuri 42ad4d6959
This closes #1906, fix a v2.8.1 regression bug introduced by commit d9a0da7b48
- Fix incorrect cell value written if save multiple times
- Update unit tests
2024-05-24 22:05:07 +08:00
nna 5f583549f4
Add unit test for the stream writer to improved line of code coverage (#1898)
- Update dependencies modules
- Using the workbook instead of XLSX in the function comments
2024-05-14 12:06:10 +08:00
xuri a64efca31f
This fixes #1888, read internal media files with absolute path 2024-05-04 20:34:22 +08:00
barlevd 781c38481d
This closes #1889, refs #1732 and #1735 (#1890)
Saving workbook with reverse sorted internal part path to keep same hash of identical files and fix incorrect MIME type
2024-05-01 00:05:05 +08:00
xuri 7715c1462a
This closes #1886, remove the namespace prefix for the default spreadsheet namespace
- Improvement compatibility for the workbook internal part with a spreadsheet namespace prefix
- Update GitHub Action configuration, using the macOS 13 in the unit test pipeline to temporarily resolve test failed in macos-14-arm64
2024-04-27 20:13:43 +08:00
xuri 055349d8a6
Fix a v2.8.1 regression bug, error on duplicate rows, if conditional formatting or data validation has multiple cell range reference
- Update unit tests
2024-04-26 00:23:10 +08:00
jianxinhou f8487a68a8
This closes #1879, compatible with the escaped quote symbol in none formula data validation rules (#1880)
- Update dependencies module to fix vulnerabilities
- Update unit tests

Co-authored-by: houjianxin.rupert <houjianxin.rupert@bytedance.com>
2024-04-18 13:21:46 +08:00
Nima 3e636ae7b2
This closes #1874, reduces memory usage for the GetRows function (#1875)
- Avoid allocate memory for reading continuously empty rows on the tail of the worksheet
2024-04-11 23:12:56 +08:00
xuri 5f8a5b8690
This closes #1867, breaking changes: change the data type for the ConditionalFormatOptions structure field Format as a pointer 2024-04-03 08:44:46 +08:00
xuri 5dc22e874b
Support get the cell images inserted by IMAGE formula function 2024-04-02 08:47:57 +08:00
xuri 9999221450
This closes #1865, unescape newline character in stream writer
- Update dependencies module
2024-04-01 08:49:21 +08:00
yunkeweb ffad7aecb5
Support get rich data value rels index from rich value part (#1866) 2024-03-28 16:37:35 +08:00
yangyile-yyle88 5e500f5e5d
Introduce new exported PictureInsertType enumeration (#1864) 2024-03-27 15:50:51 +08:00
Matthew Sackman 838232fd27
Add support for get the Microsoft 365 cell images (#1857)
- Update unit tests
2024-03-26 23:19:23 +08:00
xuri 703b73779c
This closes #1861, fix missing parentheses in the adjusted formula
- Allow adjust cell reference with max rows/columns
- Fix incorrect data validation escape result
- Update out date reference link in the documentation
- Update unit tests
2024-03-25 08:33:29 +08:00
realzuojianxiang 5975d87f7e
This closes #1851, and closes #1856 fix formula calculation result round issue (#1860)
- The SetSheetName function now support case sensitivity
- Update unit tests
2024-03-22 16:09:45 +08:00
vic 9e884c798b
This closes #1847, support apply number format with alignment (#1852)
- Update dependencies module
- Update unit tests
2024-03-19 08:58:52 +08:00
hu5ky 4eb088cf73
This fix performance impact introduced in #1692 (#1849)
Co-authored-by: chun.zhang2 <chun.zhang2@geely.com>

- This fix speed slowdown and memory usage increase base on the reverts commit 6220a798fd
- Fix panic on read workbook with internal row element without r attribute
- Update the unit tests
2024-03-15 11:36:34 +08:00
yeahyear 585ebff5b7
Typo fix for the comment of the extractStyleCondFuncs variable (#1846)
Signed-off-by: yetyear <flite@outlook.com>
2024-03-13 14:39:22 +08:00
Evan lu 4ed493819a
This closes #1835, support get data validations which storage in the extension lists (#1834) 2024-03-06 09:26:38 +08:00
xuri f20bbd1f1d
This closes #1830, closes #1831, and closes #1833
- Fix a v2.8.1 regression bug, auto filter does not work in the LibreOffice
- Fix a v2.8.1 regression bug, support to adjust data validation with multiple cell range
- Fix incorrect result data type of the DATE formula function
- Update the unit tests
2024-03-04 21:40:27 +08:00
Paolo Barbolini 963a058535
Optimize getSharedFormula to avoid runtime.duffcopy (#1837) 2024-03-03 09:39:50 +08:00
陈王 9d4c2e60f6
This closes #1825, made AddDataValidation and DeleteDataValidation functions concurrency safe (#1828)
- Remove the receiver of internal coordinatesToRangeRef, squashSqref and flatSqref functions
- Update unit tests

Co-authored-by: chenwang <chenwang@shinsson.com>
2024-03-01 10:12:17 +08:00
岳晨旭 7b4da3906d
This closes #1819, closes #1827, formula function ISNUMBER, OR and FIND support matrix arguments (#1829)
- Keep minimum column and row number in formula operand when deleting columns and rows
- Update unit tests
2024-02-29 09:16:39 +08:00
helloWorld bb603b37d0
Clear slave cells value when merging cells (#1824) 2024-02-27 16:43:47 +08:00
xuri 688808b2b4
This closes #1819, formula calculation engine support array formulas
- Improve the defined name and table name validation rules
- Rename internal variable names to avoid the same with Go 1.21's built-in min and max functions
- Simplify data type conversion in internal code
- Update GitHub Actions workflow configuration, test on Go 1.22.x, and disable Go module cache
- Update dependencies module
2024-02-26 02:22:51 +08:00
Ed 02b84a906c
This closes #1820, converted styleFillVariants from slice to func (#1821) 2024-02-24 09:11:31 +08:00
Vivek Kairi ee2ef152d9
This closes #1815, cell value reading functions inherit the Options settings of the OpenReader (#1816)
Co-authored-by: Vivek Kairi <vivek@zerodhafundhouse.com>
2024-02-15 13:30:07 +08:00
zhukewen 9cbe3b6bd0
This closes #1807, calculation engine support date and formula type cell (#1810)
Co-authored-by: zhualong <274131322@qq.com>
2024-02-05 00:06:38 +08:00
coolbit bba155e06d
This closes #1805, support set chart axis font family, size and strike style (#1809)
- Update unit test workflow dependencies package version
2024-02-04 22:31:03 +08:00
funa12 a258e3d858
Fix CalcCellValue does not return raw value when enable RawCellValue (#1803) 2024-02-02 10:11:16 +08:00
xxxwang1983 99e91e19ef
This closes #1794, add new GetBaseColor function (#1798)
Co-authored-by: wangjingwei <wang.jingwei@joyratel.com>
2024-01-30 09:58:24 +08:00
cherry 9a6855375e
This closes #1792, support to update defined names reference when rename worksheet (#1797) 2024-01-29 10:18:21 +08:00
Jerry e4497c494d
ref #65, new formula function: DBCS (#1791)
Co-authored-by: wujierui <wujierui@jimabrand.com>
2024-01-25 14:39:21 +08:00
L4nn15ter 9b078980df
This closes #1789, delete VML shape on delete comment (#1790)
- Improve delete cell comment shape compatibility with KingSoft WPS
- Update unit test
2024-01-24 14:01:56 +08:00
MELF晓宇 5399572353
This closes #1786, support set fill color of the chart (#1788)
- Add a new field Fill in Chart, ChartPlotArea, and ChartMarker struct
- Support set solid color or transparent fill for chart area, plot area, and maker
2024-01-22 09:41:57 +08:00
327674413 4eb3486682
This closes #1783, support set conditional formatting with multiple cell ranges (#1787) 2024-01-21 00:05:28 +08:00
3zmx 50e23df865
ref #65, support _xlfn.ANCHORARRAY formula function (#1784)
- Initial formula array calculation support
- Update unit test and documentation
2024-01-18 15:31:43 +08:00
xuri 792656552b
This closes #1777, fix the GetStyle or GetConditionalStyle function to returns incorrect DecimalPlaces field value
- Update documentation for the NewStyle function
- Update unit tests
- Update dependencies Go module
- Update GitHub workflow dependencies package version
- Update copyright agreement statement
2024-01-09 20:56:20 +08:00
xuri f4e395137d
This closes #1770, fix incorrect multiple conditional formats rules priorities
- Rename variable name hCell to topLeftCell, and rename vCell to bottomRightCell
- Update the unit tests
2023-12-30 14:41:16 +08:00
xuri bb8e5dacd2
This closes #1769 and closes #1770, support multiple conditional formats rules
- Update the unit tests
2023-12-28 16:38:13 +08:00
xuri 8831afc558
This recover the Sizes field in the ChartSeries data type removed in commit dfaf418f34
- Update unit tests and documentation of the internal uintPtr function
2023-12-25 21:51:09 +08:00
xuri e998c374ac
This closes #1767, change struct field tabRatio date type to float64 2023-12-22 20:49:14 +08:00
xuri 37e2d946be
Breaking changes: Go 1.18 and later required
- This made the GetPictureCell function support get embedded cell images
- Update dependencies module
- Update GitHub workflow
- Update documentation for the AddChart function
2023-12-19 23:39:45 +08:00
li 00d62590f4
This closes #664, support get embedded cell images (#1759)
Co-authored-by: liying05 <liying05@zhidemai.com>
2023-12-15 13:09:42 +08:00
yuegu520 dfaf418f34
This closes # 1704, support set the data labels position for the chart (#1755)
- Breaking change: remove the Sizes field in the ChartSeries data type
- Add new field DataLabelPosition in the ChartSeries data type, support to sets the position of the chart series data label
- Add new field BubbleSize in the Chart data type, support set the bubble size in all data series for the bubble chart or 3D bubble chart
- Add new exported ChartDataLabelPositionType data type
- Update docs and unit test for the AddChart function
- Fix a v2.7.1 regression bug, the bubble is hidden in the bubble or 3D bubble chart, commit ID: c2d6707a85
2023-12-14 00:03:53 +08:00
Xuesong 284345e471
This closes #1749, fix incorrect adjust merged cells on remove rows (#1753) 2023-12-13 09:22:41 +08:00
cui fliter 7b3dd03947
Remove unused exported struct ShapeColor (#1746)
Signed-off-by: cui fliter <imcusg@gmail.com>
2023-12-09 12:08:29 +08:00
xuri 866f3086cd
This closes #1745, prevent panic on get conditional format without above average rules
- Define internal map variable globally instead of inside of functions
2023-12-08 00:09:06 +08:00
cui fliter 77ece87e32
This fix some function names in comment (#1747)
Signed-off-by: cui fliter <imcusg@gmail.com>
2023-12-07 15:22:26 +08:00
天爱有情 18a160c5be
Support unset custom row height if height value is -1 (#1736)
- Return error if get an invalid row height value
- Update unit tests and update documentation of the SetRowHeigth function
2023-12-02 12:03:09 +08:00
user65536 a16182e004
This closes #1732, saving workbook with sorted internal part path (#1735) 2023-12-01 00:31:41 +08:00
Bram Vanbilsen 866e7fd9e1
This closes #1729, support copy conditional format and data validation on duplicate row (#1733) 2023-11-29 00:13:39 +08:00
zcgly bce2789c11
This support set column style with default width in sheet props settings (#1728) 2023-11-25 02:03:33 +08:00
xuri 41259b474f
Recalculate and use the same shared string count and unique count to overwrite incorrect existing values 2023-11-23 00:03:10 +08:00
ZX 6251d493b3
Fixed invalid shared string table index on set cell value (#1725) 2023-11-21 11:23:54 +08:00
Tian 55e4d4b2c3
The GetCellRichText function support to return inline rich text (#1724)
Co-authored-by: jintian.wang <jintian.wang@mihoyo.com>
2023-11-20 23:57:45 +08:00
lujin 6220a798fd
This closes #1723, fix panic on read workbook in some cases (#1692)
- Fix panic on read workbook with internal row element without r attribute
- Check worksheet XML before get all cell value by column or row
- Update the unit tests
2023-11-18 16:44:45 +08:00
Tajang 57faaf253a
Fix panic on GetPictureCells without drawing relationships parts (#1721) 2023-11-16 09:34:37 +08:00
15535382838 3bdc2c5fc7
This add new exported function GetHeaderFooter (#1720) 2023-11-15 12:49:18 +08:00
Yang Li 5e247de805
Support set time period type conditional formatting (#1718) 2023-11-14 09:47:57 +08:00
xuri 2499bf6b5b
Support 5 new kinds of conditional formatting types
- New conditional formatting types: text, blanks, no blanks, errors, and no errors
- Support calculate formula with multiple dash arithmetic symbol
- Fix empty calculate result with numeric arguments in LEN, LOWER, PROPER, REPT, UPPER, and IF formula functions
- Uniform double quote in calculation unit tests
- Update unit tests
2023-11-13 00:16:29 +08:00
xuri c7acf4fafe
Support update data validations on inserting/deleting columns/rows 2023-11-11 00:04:05 +08:00
ByteFlyCoding e014a8bb23
Support update conditional formatting on inserting/deleting columns/rows (#1717)
Return error for unsupported conditional formatting rule types
2023-11-10 09:25:59 +08:00
Nick 134865d9d2
Update data validation type and operator's enumerations (#1714) 2023-11-09 00:15:10 +08:00
xuri a0252bd05a
Support update defined names on inserting/deleting columns/rows 2023-11-08 00:01:35 +08:00
Marko Krstic 6cc1a547ab
This closes #1712, reduce memory consumption (#1713) 2023-11-07 08:46:01 +08:00
Anton Petrov fe639faa45
This closes #1125, support update drawing objects on inserting/deleting columns/rows (#1127) 2023-11-06 09:51:19 +08:00
magicrabbit f753e560fa
Fix number format scientific notation zero fill issues (#1710) 2023-11-04 08:02:09 +08:00
xuri 4e936dafdd
This closes #1706 and closes #1708
- Add export ChartLineType enumeration to specify the chart line type
- Add new Border field in the Chart type to set the chart area border
- Add new Type field in the ChartLine type to set the line type
- Fix some format missing on get style definition
- Update the unit tests
2023-11-03 00:12:43 +08:00
xuri 7291e787bd
Support update volatile dependencies on inserting/deleting columns/rows 2023-11-02 00:15:41 +08:00
rjtee b41a6cc3cd
Support to adjust formula cross worksheet on inserting/deleting columns/rows (#1705) 2023-11-01 00:52:18 +08:00
xuri 5bba8f9805
This improves compatibility for adjusting tables and formula references on inserting/deleting columns or rows 2023-10-31 00:01:57 +08:00
rjtee cf3e0164d9
Support adjust formula for entire cols/rows reference (#1702)
- Update the unit tests
2023-10-29 13:40:21 +08:00
rjtee a8cbcfa39b
This closes #1306 and closes #1615 (#1698)
- Support adjust formula on inserting/deleting columns/rows
2023-10-24 00:05:52 +08:00
xuri 05689d6ade
This closes #1694, using namespace prefix in workbook theme XML
- Improve compatibility with the viewer which doesn't support default theme part namespace
- ref #1690, support read background color style, and conditional format with default pattern type
- Update the unit tests
2023-10-20 00:04:31 +08:00
xuri b52db71d61
This closes #1690, new exported function `GetConditionalStyle`
- Support get the conditional format style definition
- Update the unit test
2023-10-17 08:52:34 +08:00
壹次心 27f1056929
This closes #1218 and closes #1689 (#1634)
- Introduce new exported function GetPictureCells
- Upgrade dependencies module golang.org/x/net from 0.16.0 to 0.17.0
- Update unit tests
2023-10-13 00:06:07 +08:00
Eng Zer Jun f752f2ddf4
Remove redundant `len` check in `GroupSheets` and `UngroupSheets` (#1685) 2023-10-12 09:23:31 +08:00
xuri d9a0da7b48
This closes #1687 and closes #1688
- Using sync map internally to get cell value concurrency safe
- Support set the height and width for the comment box
- Update the unit test
2023-10-11 00:04:38 +08:00
xuri d133dc12d7
Support format cell value with fraction number format code
- Fix delete incorrect image files when picture deleting pictures
- Update the unit test and dependencies modules
2023-10-10 00:04:10 +08:00
xuri 87a00e4f7e
This closed #1680, fixing a potential issue that stream reader temporary files can not be clear
- Delete image files from the workbook internally when deleting pictures to reduce generated workbook size and resolve potential security issues
2023-10-09 00:14:56 +08:00
xuri 99df1a7343
This closes #1681, closes #1683
- Fix incorrect formula calculation result
- Introduce new exported function `SetCellUint`
- Updates unit test
- Typo fixed
2023-10-08 00:06:11 +08:00
Abdelaziz-Ouhammou 07f2c6831a
Keep all cells value in the table range when deleting table (#1684) 2023-10-07 00:05:50 +08:00
xuri 95fc35f46c
This fix #1682, removes table and content type parts when deleting the table
- Move worksheet-related functions in one place
2023-10-06 00:04:38 +08:00
xuri df032fcae7
This improves performance for adding and removing pivot table 2023-10-05 00:08:31 +08:00
dependabot[bot] ecb4f62b77
Update actions/checkout from 3 to 4 (#1676) 2023-10-04 00:11:50 +08:00
xuri 0861faf2f2
Add new exported function `DeletePivotTable`
- Support adding pivot table by specific table name
- Update unit tests
2023-10-03 00:59:31 +08:00
xuri 1c7c417c70
This closes #1677, fix the incorrect custom number format ID allocated
- Improve compatibility with empty custom number format code
2023-10-02 00:06:38 +08:00
xuri f85770f4c9
ref #65, formula functions TEXTAFTER and TEXTBEFORE (array formula not support yet) 2023-10-01 13:37:47 +08:00
xuri 1c23dc3507
This sorted exported error constants by name and listed them in one place 2023-09-28 08:53:54 +08:00
xuri c62d23e0a1
The `AddSlicer` function now support create pivot table slicer 2023-09-27 00:05:59 +08:00
xuri 9c079e5eec
This fix #1665, supports getting formula string cell value
- Improve compatibility for absolute path drawing part
- Fix incorrect table ID generated in the workbook which contains single table cells
- Fix missing relationship parts in the content types in some cases
- Upgrade number format parser to fix missing literal tokens in some cases
- Update built-in zh-cn and zh-tw language number format
- Ref #65, init new formula function: TEXT
- Remove duplicate style-related variables
- Update the unit tests
2023-09-21 00:06:31 +08:00
xuri 744236b4b8
This closes #1661, fix incorrect time number format result 2023-09-17 18:30:58 +08:00
xuri e3b7dad69a
Introduce the new exported function `AddSlicer` for adding table slicer
- Fix a v2.8.0 regression bug, generate workbook corruption caused by incorrect MRU colors style parts
- Fix corrupted workbooks generated when adding tables in some cases
- Added several exported extension list child element URI constants
- Move part of the internal constant and variables definition to the template source code file
- Updated unit tests
2023-09-16 12:21:11 +08:00
xuri 5a039f3045
This fixes #1658
- Fix a v2.8.0 regression bug, number format code apply result was empty
- Fix calculate formula functions CHITEST and MMULT panic in some cases
- Updated unit tests
2023-09-14 22:56:28 +08:00
xuri 49706c9018
This fixes #1645 and fixes #1655
- Breaking changes, change the data type for the `HeaderFooterOptions` structure fields `AlignWithMargins` and `ScaleWithDoc` as a pointer
- Fixed panic on `AutoFilter` by adding nil pointer guard for local sheet ID
- Allow dot character in the defined name, table name, or pivot table name
- Update the unit tests
2023-09-09 13:51:00 +08:00
Matthias Endler a0a7d5cdbb
Fix bug in checkDefinedNames 2023-09-09 13:06:22 +08:00
xuri ae64bcaabe
This fixes #1643, fixes #1647 and fixes #1653
- Correction cell type when formatting date type cell value
- Add check for MID and MIDB formula functions num_chars arguments, prevent panic on specifying a negative number
- Ref #65, add support for 2 formula functions: SEARCH and SEARCHB
- Fix a v2.8.0 regression bug, error on set print area and print titles with built-in special defined name
- Add new exported function `GetPivotTables` for get pivot tables
- Add a new `Name` field in the `PivotTableOptions` to support specify pivot table name
- Using relative cell reference in the pivot table docs and unit tests
- Support adding slicer content type part internally
- Add new exported source relationship and namespace `NameSpaceSpreadSheetXR10`, `ContentTypeSlicer`, `ContentTypeSlicerCache`, and `SourceRelationshipSlicer`
- Add new exported extended URI `ExtURIPivotCacheDefinition`
- Fix formula argument wildcard match issues
- Update GitHub Actions configuration, test on Go 1.21.x with 1.21.1 and later
- Avoid corrupted workbooks generated by improving compatibility with internally indexed color styles
2023-09-08 00:21:38 +08:00
Francis Nickels III ff5657ba87
add Footer & Header clarification to docs (#1644) 2023-09-04 00:11:35 +08:00
xuri 3b2b8ca8d6
Update the README and documentation for the data validation functions 2023-08-28 00:02:25 +08:00
fsfsx 4957ee9abc
ref #65, add support for 10 formula functions
- Add support for 10 formula functions: ARRAYTOTEXT, FORECAST, FORECAST.LINEAR, FREQUENCY, INTERCEPT, ODDFYIELD, ODDLPRICE, ODDLYIELD, PROB and VALUETOTEXT
- Update unit tests
2023-08-26 13:14:03 +08:00
xuri 15614badfc
This closes #1628, fix the GetPictures function returns pictures doesn't correct in some cases 2023-08-25 01:06:41 +08:00
cnmlgbgithub db22452398
This closes #314, closes #1520 and closes #1521 (#1574)
- Add new function GetStyle support for get style definition
2023-08-24 23:51:07 +08:00
fsfsx cb5a8e2d1e
This closes #674, closes #1454, add new exported functions GetTables and DeleteTable (#1573) 2023-08-23 10:51:11 +08:00
xuri 1b63d098a7
This improves applying cell value with currency and accounting number format
- Update the unit test and dependencies modules
2023-08-21 00:11:55 +08:00
xuri c63ae6d262
This fixed #1610, support to create a conditional format with number format and protection 2023-08-17 11:34:28 +08:00
xuri a1810aa056
This improves the date and time number formats
- Now support applying date and time number format for 812 language tags
- Fix panic on getting merged cells with the same start and end axis
2023-08-15 00:01:57 +08:00
xuri ae17fa87d5
This ref #1585, support to read one cell anchor pictures and improve date and time number format
- Support apply date and time number format with 16 languages: Persian, Polish, Portuguese, Punjabi, Quechua, Romanian, Romansh, Sakha, Sami, Sanskrit, Scottish Gaelic, Serbian, Sesotho sa Leboa, Setswana, Sindhi, Sinhala and Slovak
- Update the unit test and dependencies modules
2023-08-09 00:11:06 +08:00
xuri eb175906e7
This fixes #1599, and improve date and time number format
- Fix basic arithmetic operator priority issues
- Support apply date and time number format with 52 languages: Estonian, Faroese, Filipino, Finnish, Frisian, Fulah, Galician, Georgian, Greek, Greenlandic, Guarani, Gujarati, Hausa, Hawaiian, Hebrew, Hindi, Hungarian, Icelandic, Igbo, Indonesian, Inuktitut, Kannada, Kashmiri, Kazakh, Khmer, Kiche, Kinyarwanda, Kiswahili, Konkani, Kyrgyz, Lao, Latin, Latvian, Lithuanian, Luxembourgish, Macedonian, Malay, Malayalam, Maltese, Maori, Mapudungun, Marathi, Mohawk, Morocco, Nepali, Nigeria, Norwegian, Occitan, Odia, Oromo, Pashto and Syllabics
- Support apply the Chinese weekdays' number formats
- Update the unit test and dependencies modules
2023-08-06 00:02:48 +08:00
xuri aa3c79a811
Support apply number format with the Japanese era years 2023-08-01 00:11:02 +08:00
xuri 5fe30eb456
This closes #1590, add the Japanese calendar number format support
- The `GetFormControl` now support to get text, rich-text and font format of the form controls
- Update the unit tests and the documentation
2023-07-31 00:08:10 +08:00
xuri a07c8cd0b4
This closes #1588, closes #1591, breaking changes for the `AddChart` function
- Removed exported `ChartTitle` data type
- The `AddChart` function now supports formatting and setting rich text titles for the chart
- New exported function `GetFormControl` for getting form control
- Made case in-sensitive for internal worksheet XML path to improve compatibility
- Update the unit tests
- Update the documentation and internal comments on the codes
2023-07-28 00:24:08 +08:00
xuri 2e9c2904f2
This closes #1587, fix incorrect date time format result 2023-07-27 00:03:15 +08:00
xuri 7f3d663628
This closes #1584, fix graphic object counter issues
- Optimize number format parser
- Update documentation for the `AddFormControl` function
- Update unit tests
- Upgrade dependencies package
2023-07-25 00:08:24 +08:00
xuri 8d996ca138
This closes #1582, fixes the formula calculation bug, and improves form controls
- Fix incorrect formula calculate results on a nested argument function which returns a numeric result
- Add a new exported error variable `ErrorFormControlValue`
- Rename exported enumeration `FormControlCheckbox` to `FormControlCheckBox`
- Rename exported enumeration `FormControlRadio` to `FormControlOptionButton`
- The `AddFormControl` function supports new 5 form controls: spin button, check box, group box, label, and scroll bar
- Update documentation for the `GraphicOptions` data type, `AddFormControl` and `NewStreamWriter` functions
- Update the unit tests
2023-07-21 00:03:37 +08:00
xuri b667987084
This closes #301, support delete and add radio button form control
- New exported function `DeleteFormControl` has been added
- Update unit tests
- Fix comments was missing after form control added
- Update pull request templates
2023-07-13 00:03:24 +08:00
David 2c8dc5c150
This closes #1169, initialize add form controls supported (#1181)
- Breaking changes:
* Change
    `func (f *File) AddShape(sheet, cell string, opts *Shape) error`
    to
    `func (f *File) AddShape(sheet string, opts *Shape) error`
* Rename the `Runs` field to `Paragraph` in the exported `Comment` data type
- Add new exported function `AddFormControl` support to add button and radio form controls
- Add check for shape type for the `AddShape` function, an error will be returned if no shape type is specified
- Updated functions documentation and the unit tests
2023-07-11 23:43:45 +08:00
xuri 8418bd7afd
This closes #1572
- Breaking changes: changed the data type for the `DecimalPlaces` to pointer of integer
- Fallback to default 2 zero placeholder for invalid decimal places
- Update unit tests
2023-07-08 18:36:35 +08:00
xuri f5fe6d3fc9
This closes #518, support creating chart with a secondary series axis 2023-07-07 11:00:54 +00:00
xuri fb72e56667
This closes #1569, formula function CONCAT, CONCATENATE support concatenation of multiple cell values 2023-07-06 10:49:49 +00:00
lidp20 e2c7416292
This closes #1565, support adjust formula when instering columns and rows (#1567) 2023-07-04 00:06:37 +08:00
xuri 700af6a529
This fixed #1564, apply all of its arguments that meet multiple criteria 2023-07-03 00:05:26 +08:00
xuri dcb26b2cb8
Made unit tests compatibility with the next Go language version
- Fix documents issues for the `AddChart` function
- Update GitHub sponsor profile
2023-06-30 05:19:00 +00:00
xuri f8aa3adf7e
This closes #1553, the `AddChart` function support set primary titles
- Update unit tests and documentation
- Lint issues fixed
2023-06-18 00:13:03 +08:00
chengxinyao 9bc3fd7e9f
This optimize the code, simplify unit test and drawing object position calculation (#1561)
Co-authored-by: xinyao.cheng <xinyao.cheng@zerone.com.cn>
2023-06-14 22:49:40 +08:00
xuri 8e891b52c6
This closes #1560, fix incorrect row number when get object position 2023-06-12 00:09:40 +08:00
vb6iscool 78c974d855
New function `GetPanes` for get sheet panes and view selection (#1556)
- Breaking changes: rename exported type `PaneOptions` to `Selection`
- Update unit tests
- Upgrade dependencies package
- Add internal error variables
- Simplify variable declarations
2023-06-08 09:50:38 +08:00
xuri 661c0eade9
Support apply built-in number format code 22 with custom short date pattern 2023-06-05 00:06:27 +08:00
xuri 121ac17ca0
This fixed incorrect formula calculation exception expected result
- Simplify and remove duplicate code for optimization
- Update documentation comments with typo fix
- Handle error return to save the workbook
- Add file path length limitation details in the error message
2023-05-30 00:19:12 +08:00
壹次心 e3fb2d7bad
This closes #1548, support to get multiple images in one cell (#1549) 2023-05-28 00:46:34 +08:00
joehan109 16efeae5b1
This fix date and time pattern validation issues (#1547) 2023-05-27 00:22:35 +08:00
xuri 76cd0992b0
This closes #1539, fix adjust table issue when after removing rows 2023-05-23 00:18:55 +08:00
Eng Zer Jun c232748400
This avoid unnecessary byte/string conversion (#1541)
Signed-off-by: Eng Zer Jun <engzerjun@gmail.com>
2023-05-22 09:24:28 +08:00
xuri a246db6a40
This closes #279, refs #1536, change the default point to pixels conversion factor 2023-05-19 19:53:18 +08:00
xuri 08ba2723fe
This closes #1536, support fallback to default column width in sheet format property 2023-05-18 20:33:16 +08:00
xuri ef3e81de8e
This fixed across worksheet reference issue for the formula calculation engine 2023-05-17 00:05:27 +08:00
xuri 1088302331
This closes #1535, add documentation for the fields for style alignment 2023-05-16 09:44:08 +08:00
xuri 49234fb95e
Ref #1199, this support applies partial built-in language number format code
- Remove the `Lang` field in the `Style` data type
- Rename field name `ShortDateFmtCode` to `ShortDatePattern` in the `Options` data type
- Rename field name `LongDateFmtCode` to `LongDatePattern` in the `Options` data type
- Rename field name `LongTimeFmtCode` to `LongTimePattern` in the `Options` data type
- Apply built-in language number format code number when creating a new style
- Checking and returning error if the date and time pattern was invalid
- Add new `Options` field `CultureInfo` and new exported data type `CultureName`
- Add new culture name types enumeration for country code
- Update unit tests
- Move built-in number format code and currency number format code definition source code
- Remove the built-in language number format code mapping with Unicode values
- Fix incorrect number formatted result for date and time with 12 hours at AM
2023-05-11 09:08:38 +08:00
fudali dfdd97c0a7
This closes #1199, support apply number format by system date and time options
- Add new options `ShortDateFmtCode`, `LongDateFmtCode` and `LongTimeFmtCode`
- Update unit tests
2023-05-06 20:34:18 +08:00
xuri bbdb83abf0
This closes #660, supports currency string, and switches argument for the number format code
- Support round millisecond for the date time
- Update built-in number formats mapping
- Update unit tests
- Upgrade dependencies package
2023-05-04 02:52:26 +00:00
xuri 7c221cf295
Ref #660, support placeholder, padding and rounds numbers by specified number format code
- Remove built-in number formats functions
- Update unit tests
- Upgrade dependencies package
2023-04-30 11:10:51 +08:00
xuri 65fc25e7a6
Ref #1533, this made number format text handler just handle text tokens
- Fix race conditions for concurrency read and write shared string table
- Unit tests has been updated
2023-04-26 00:04:47 +08:00
xuri 612f6f104c
This closes #1528, closes #1533
- Avoid format text cell value as a numeric
- Fix race conditions for concurrency safety functions
2023-04-25 08:44:41 +08:00
xuri 93c72b4d55
This optimizes internal functions signature and mutex declarations 2023-04-24 00:02:13 +08:00
Chen Zhidong 787453c6f0
Optimizing regexp calls to improve performance (#1532) 2023-04-23 18:00:31 +08:00
xuri 63d8a09082
Breaking changes: rename exported variable `ErrTableNameLength` to `ErrNameLength`
- Check the defined name
- Improve the cell comment box shape size compatibility with KingSoft WPS
- Update unit test
2023-04-21 08:51:04 +08:00
xuri fb6ce60bd5
This closes #1523, preventing format text cell value as a numeric
- Simplify variable declaration and error return statements
- Remove the internal `xlsxTabColor` data type
- Using the `xlsxColor` data type instead of `xlsxTabColor`
- Update unit test, improve code coverage
2023-04-19 00:05:59 +08:00
xuri d0ad0f39ec
This commit contains 5 changes:
- Fix incorrect comment box size for multi-line plain text comments
- Prevent create duplicate tables with the same name
- Add new exported error variable `ErrExistsTableName`
- Allocate buffer inside escape XML characters
- Update the unit tests
2023-04-17 08:48:30 +08:00
xuri 17c029494a
This closes #1519, escape XML characters after checking cell value length 2023-04-16 14:22:55 +08:00
Valery Ozarnichuk 635ec33576
Support checking cell value length with multi-bytes characters (#1517) 2023-04-12 08:17:10 +08:00
dependabot[bot] 4196348f9f
Upgrade actions/setup-go from 3 to 4 (#1512) 2023-04-11 00:27:17 +08:00
92 changed files with 26525 additions and 7155 deletions

3
.github/FUNDING.yml vendored
View File

@ -1,5 +1,6 @@
patreon: xuri github: xuri
open_collective: excelize open_collective: excelize
patreon: xuri
ko_fi: xurime ko_fi: xurime
liberapay: xuri liberapay: xuri
issuehunt: xuri issuehunt: xuri

View File

@ -20,16 +20,16 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v2 uses: github/codeql-action/init@v3
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v2 uses: github/codeql-action/autobuild@v3
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2 uses: github/codeql-action/analyze@v3

View File

@ -5,8 +5,8 @@ jobs:
test: test:
strategy: strategy:
matrix: matrix:
go-version: [1.16.x, 1.17.x, 1.18.x, 1.19.x, 1.20.x] go-version: [1.18.x, 1.19.x, 1.20.x, '>=1.21.1', 1.22.x, 1.23.x]
os: [ubuntu-latest, macos-latest, windows-latest] os: [ubuntu-latest, macos-13, windows-latest]
targetplatform: [x86, x64] targetplatform: [x86, x64]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
@ -14,12 +14,13 @@ jobs:
steps: steps:
- name: Install Go - name: Install Go
uses: actions/setup-go@v3 uses: actions/setup-go@v5
with: with:
go-version: ${{ matrix.go-version }} go-version: ${{ matrix.go-version }}
cache: false
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Get dependencies - name: Get dependencies
run: | run: |
@ -28,10 +29,12 @@ jobs:
run: go build -v . run: go build -v .
- name: Test - name: Test
run: env GO111MODULE=on go test -v -timeout 30m -race ./... -coverprofile=coverage.txt -covermode=atomic run: env GO111MODULE=on go test -v -timeout 30m -race ./... -coverprofile='coverage.txt' -covermode=atomic
- name: Codecov - name: Codecov
uses: codecov/codecov-action@v3 uses: codecov/codecov-action@v4
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with: with:
file: coverage.txt file: coverage.txt
flags: unittests flags: unittests

View File

@ -1,6 +1,6 @@
BSD 3-Clause License BSD 3-Clause License
Copyright (c) 2016-2023 The excelize Authors. Copyright (c) 2016-2024 The excelize Authors.
Copyright (c) 2011-2017 Geoffrey J. Teale Copyright (c) 2011-2017 Geoffrey J. Teale
All rights reserved. All rights reserved.

View File

@ -21,7 +21,7 @@
<!--- Please describe in detail how you tested your changes. --> <!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, and the tests you ran to --> <!--- Include details of your testing environment, and the tests you ran to -->
<!--- see how your change affects other areas of the code, etc. --> <!--- See how your change affects other areas of the code, etc. -->
## Types of changes ## Types of changes

View File

@ -13,7 +13,7 @@
## Introduction ## Introduction
Excelize is a library written in pure Go providing a set of functions that allow you to write to and read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and writing spreadsheet documents generated by Microsoft Excel&trade; 2007 and later. Supports complex components by high compatibility, and provided streaming API for generating or reading data from a worksheet with huge amounts of data. This library needs Go version 1.16 or later. The full docs can be seen using go's built-in documentation tool, or online at [go.dev](https://pkg.go.dev/github.com/xuri/excelize/v2) and [docs reference](https://xuri.me/excelize/). Excelize is a library written in pure Go providing a set of functions that allow you to write to and read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and writing spreadsheet documents generated by Microsoft Excel&trade; 2007 and later. Supports complex components by high compatibility, and provided streaming API for generating or reading data from a worksheet with huge amounts of data. This library needs Go version 1.18 or later. There are some [incompatible changes](https://github.com/golang/go/issues/61881) in the Go 1.21.0, the Excelize library can not working with that version normally, if you are using the Go 1.21.x, please upgrade to the Go 1.21.1 and later version. The full docs can be seen using go's built-in documentation tool, or online at [go.dev](https://pkg.go.dev/github.com/xuri/excelize/v2) and [docs reference](https://xuri.me/excelize/).
## Basic Usage ## Basic Usage
@ -165,8 +165,10 @@ func main() {
Categories: "Sheet1!$B$1:$D$1", Categories: "Sheet1!$B$1:$D$1",
Values: "Sheet1!$B$4:$D$4", Values: "Sheet1!$B$4:$D$4",
}}, }},
Title: excelize.ChartTitle{ Title: []excelize.RichTextRun{
Name: "Fruit 3D Clustered Column Chart", {
Text: "Fruit 3D Clustered Column Chart",
},
}, },
}); err != nil { }); err != nil {
fmt.Println(err) fmt.Println(err)

View File

@ -13,7 +13,7 @@
## 简介 ## 简介
Excelize 是 Go 语言编写的用于操作 Office Excel 文档基础库,基于 ECMA-376ISO/IEC 29500 国际标准。可以使用它来读取、写入由 Microsoft Excel&trade; 2007 及以上版本创建的电子表格文档。支持 XLAM / XLSM / XLSX / XLTM / XLTX 等多种文档格式,高度兼容带有样式、图片(表)、透视表、切片器等复杂组件的文档,并提供流式读写函数,用于处理包含大规模数据的工作簿。可应用于各类报表平台、云计算、边缘计算等系统。使用本类库要求使用的 Go 语言为 1.16 或更高版本,完整的使用文档请访问 [go.dev](https://pkg.go.dev/github.com/xuri/excelize/v2) 或查看 [参考文档](https://xuri.me/excelize/)。 Excelize 是 Go 语言编写的用于操作 Office Excel 文档基础库,基于 ECMA-376ISO/IEC 29500 国际标准。可以使用它来读取、写入由 Microsoft Excel&trade; 2007 及以上版本创建的电子表格文档。支持 XLAM / XLSM / XLSX / XLTM / XLTX 等多种文档格式,高度兼容带有样式、图片(表)、透视表、切片器等复杂组件的文档,并提供流式读写函数,用于处理包含大规模数据的工作簿。可应用于各类报表平台、云计算、边缘计算等系统。使用本类库要求使用的 Go 语言为 1.18 或更高版本请注意Go 1.21.0 中存在[不兼容的更改](https://github.com/golang/go/issues/61881),导致 Excelize 基础库无法在该版本上正常工作,如果您使用的是 Go 1.21.x请升级到 Go 1.21.1 及更高版本。完整的使用文档请访问 [go.dev](https://pkg.go.dev/github.com/xuri/excelize/v2) 或查看 [参考文档](https://xuri.me/excelize/)。
## 快速上手 ## 快速上手
@ -165,8 +165,10 @@ func main() {
Categories: "Sheet1!$B$1:$D$1", Categories: "Sheet1!$B$1:$D$1",
Values: "Sheet1!$B$4:$D$4", Values: "Sheet1!$B$4:$D$4",
}}, }},
Title: excelize.ChartTitle{ Title: []excelize.RichTextRun{
Name: "Fruit 3D Clustered Column Chart", {
Text: "Fruit 3D Clustered Column Chart",
},
}, },
}); err != nil { }); err != nil {
fmt.Println(err) fmt.Println(err)

903
adjust.go

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

2206
calc.go

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in // this source code is governed by a BSD-style license that can be found in
// the LICENSE file. // the LICENSE file.
// //
@ -7,7 +7,7 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming // Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of // API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.16 or later. // data. This library needs Go version 1.18 or later.
package excelize package excelize
@ -58,8 +58,8 @@ func (f *File) deleteCalcChain(index int, cell string) error {
if err != nil { if err != nil {
return err return err
} }
content.Lock() content.mu.Lock()
defer content.Unlock() defer content.mu.Unlock()
for k, v := range content.Overrides { for k, v := range content.Overrides {
if v.PartName == "/xl/calcChain.xml" { if v.PartName == "/xl/calcChain.xml" {
content.Overrides = append(content.Overrides[:k], content.Overrides[k+1:]...) content.Overrides = append(content.Overrides[:k], content.Overrides[k+1:]...)
@ -81,3 +81,39 @@ func (c xlsxCalcChainCollection) Filter(fn func(v xlsxCalcChainC) bool) []xlsxCa
} }
return results return results
} }
// volatileDepsReader provides a function to get the pointer to the structure
// after deserialization of xl/volatileDependencies.xml.
func (f *File) volatileDepsReader() (*xlsxVolTypes, error) {
if f.VolatileDeps == nil {
volatileDeps, ok := f.Pkg.Load(defaultXMLPathVolatileDeps)
if !ok {
return f.VolatileDeps, nil
}
f.VolatileDeps = new(xlsxVolTypes)
if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(volatileDeps.([]byte)))).
Decode(f.VolatileDeps); err != nil && err != io.EOF {
return f.VolatileDeps, err
}
}
return f.VolatileDeps, nil
}
// volatileDepsWriter provides a function to save xl/volatileDependencies.xml
// after serialize structure.
func (f *File) volatileDepsWriter() {
if f.VolatileDeps != nil {
output, _ := xml.Marshal(f.VolatileDeps)
f.saveFileList(defaultXMLPathVolatileDeps, output)
}
}
// deleteVolTopicRef provides a function to remove cell reference on the
// volatile dependencies topic.
func (vt *xlsxVolTypes) deleteVolTopicRef(i1, i2, i3, i4 int) {
for i := range vt.VolType[i1].Main[i2].Tp[i3].Tr {
if i == i4 {
vt.VolType[i1].Main[i2].Tp[i3].Tr = append(vt.VolType[i1].Main[i2].Tp[i3].Tr[:i], vt.VolType[i1].Main[i2].Tp[i3].Tr[i+1:]...)
}
}
}

432
cell.go
View File

@ -1,4 +1,4 @@
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in // this source code is governed by a BSD-style license that can be found in
// the LICENSE file. // the LICENSE file.
// //
@ -7,7 +7,7 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming // Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of // API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.16 or later. // data. This library needs Go version 1.18 or later.
package excelize package excelize
@ -15,11 +15,13 @@ import (
"bytes" "bytes"
"encoding/xml" "encoding/xml"
"fmt" "fmt"
"math"
"os" "os"
"reflect" "reflect"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"unicode/utf8"
) )
// CellType is the type of cell value type. // CellType is the type of cell value type.
@ -71,7 +73,7 @@ func (f *File) GetCellValue(sheet, cell string, opts ...Options) (string, error)
if err != nil { if err != nil {
return "", true, err return "", true, err
} }
val, err := c.getValueFrom(f, sst, getOptions(opts...).RawCellValue) val, err := c.getValueFrom(f, sst, f.getOptions(opts...).RawCellValue)
return val, true, err return val, true, err
}) })
} }
@ -142,7 +144,7 @@ func (f *File) SetCellValue(sheet, cell string, value interface{}) error {
if err != nil { if err != nil {
return err return err
} }
err = f.setDefaultTimeStyle(sheet, cell, 21) err = f.setDefaultTimeStyle(sheet, cell, getDurationNumFmt(v))
case time.Time: case time.Time:
err = f.setCellTimeFunc(sheet, cell, v) err = f.setCellTimeFunc(sheet, cell, v)
case bool: case bool:
@ -215,15 +217,15 @@ func (f *File) setCellIntFunc(sheet, cell string, value interface{}) error {
case int64: case int64:
err = f.SetCellInt(sheet, cell, int(v)) err = f.SetCellInt(sheet, cell, int(v))
case uint: case uint:
err = f.SetCellInt(sheet, cell, int(v)) err = f.SetCellUint(sheet, cell, uint64(v))
case uint8: case uint8:
err = f.SetCellInt(sheet, cell, int(v)) err = f.SetCellUint(sheet, cell, uint64(v))
case uint16: case uint16:
err = f.SetCellInt(sheet, cell, int(v)) err = f.SetCellUint(sheet, cell, uint64(v))
case uint32: case uint32:
err = f.SetCellInt(sheet, cell, int(v)) err = f.SetCellUint(sheet, cell, uint64(v))
case uint64: case uint64:
err = f.SetCellInt(sheet, cell, int(v)) err = f.SetCellUint(sheet, cell, v)
} }
return err return err
} }
@ -235,13 +237,13 @@ func (f *File) setCellTimeFunc(sheet, cell string, value time.Time) error {
if err != nil { if err != nil {
return err return err
} }
c, col, row, err := f.prepareCell(ws, cell) c, col, row, err := ws.prepareCell(cell)
if err != nil { if err != nil {
return err return err
} }
ws.Lock() ws.mu.Lock()
c.S = f.prepareCellStyle(ws, col, row, c.S) c.S = ws.prepareCellStyle(col, row, c.S)
ws.Unlock() ws.mu.Unlock()
var date1904, isNum bool var date1904, isNum bool
wb, err := f.workbookReader() wb, err := f.workbookReader()
if err != nil { if err != nil {
@ -254,7 +256,7 @@ func (f *File) setCellTimeFunc(sheet, cell string, value time.Time) error {
return err return err
} }
if isNum { if isNum {
_ = f.setDefaultTimeStyle(sheet, cell, 22) _ = f.setDefaultTimeStyle(sheet, cell, getTimeNumFmt(value))
} }
return err return err
} }
@ -287,50 +289,84 @@ func setCellDuration(value time.Duration) (t string, v string) {
// SetCellInt provides a function to set int type value of a cell by given // SetCellInt provides a function to set int type value of a cell by given
// worksheet name, cell reference and cell value. // worksheet name, cell reference and cell value.
func (f *File) SetCellInt(sheet, cell string, value int) error { func (f *File) SetCellInt(sheet, cell string, value int) error {
f.mu.Lock()
ws, err := f.workSheetReader(sheet) ws, err := f.workSheetReader(sheet)
if err != nil { if err != nil {
f.mu.Unlock()
return err return err
} }
c, col, row, err := f.prepareCell(ws, cell) f.mu.Unlock()
ws.mu.Lock()
defer ws.mu.Unlock()
c, col, row, err := ws.prepareCell(cell)
if err != nil { if err != nil {
return err return err
} }
ws.Lock() c.S = ws.prepareCellStyle(col, row, c.S)
defer ws.Unlock()
c.S = f.prepareCellStyle(ws, col, row, c.S)
c.T, c.V = setCellInt(value) c.T, c.V = setCellInt(value)
c.IS = nil c.IS = nil
return f.removeFormula(c, ws, sheet) return f.removeFormula(c, ws, sheet)
} }
// setCellInt prepares cell type and string type cell value by a given // setCellInt prepares cell type and string type cell value by a given integer.
// integer.
func setCellInt(value int) (t string, v string) { func setCellInt(value int) (t string, v string) {
v = strconv.Itoa(value) v = strconv.Itoa(value)
return return
} }
// SetCellUint provides a function to set uint type value of a cell by given
// worksheet name, cell reference and cell value.
func (f *File) SetCellUint(sheet, cell string, value uint64) error {
f.mu.Lock()
ws, err := f.workSheetReader(sheet)
if err != nil {
f.mu.Unlock()
return err
}
f.mu.Unlock()
ws.mu.Lock()
defer ws.mu.Unlock()
c, col, row, err := ws.prepareCell(cell)
if err != nil {
return err
}
c.S = ws.prepareCellStyle(col, row, c.S)
c.T, c.V = setCellUint(value)
c.IS = nil
return f.removeFormula(c, ws, sheet)
}
// setCellUint prepares cell type and string type cell value by a given unsigned
// integer.
func setCellUint(value uint64) (t string, v string) {
v = strconv.FormatUint(value, 10)
return
}
// SetCellBool provides a function to set bool type value of a cell by given // SetCellBool provides a function to set bool type value of a cell by given
// worksheet name, cell reference and cell value. // worksheet name, cell reference and cell value.
func (f *File) SetCellBool(sheet, cell string, value bool) error { func (f *File) SetCellBool(sheet, cell string, value bool) error {
f.mu.Lock()
ws, err := f.workSheetReader(sheet) ws, err := f.workSheetReader(sheet)
if err != nil { if err != nil {
f.mu.Unlock()
return err return err
} }
c, col, row, err := f.prepareCell(ws, cell) f.mu.Unlock()
ws.mu.Lock()
defer ws.mu.Unlock()
c, col, row, err := ws.prepareCell(cell)
if err != nil { if err != nil {
return err return err
} }
ws.Lock() c.S = ws.prepareCellStyle(col, row, c.S)
defer ws.Unlock()
c.S = f.prepareCellStyle(ws, col, row, c.S)
c.T, c.V = setCellBool(value) c.T, c.V = setCellBool(value)
c.IS = nil c.IS = nil
return f.removeFormula(c, ws, sheet) return f.removeFormula(c, ws, sheet)
} }
// setCellBool prepares cell type and string type cell value by a given // setCellBool prepares cell type and string type cell value by a given boolean
// boolean value. // value.
func setCellBool(value bool) (t string, v string) { func setCellBool(value bool) (t string, v string) {
t = "b" t = "b"
if value { if value {
@ -350,43 +386,55 @@ func setCellBool(value bool) (t string, v string) {
// var x float32 = 1.325 // var x float32 = 1.325
// f.SetCellFloat("Sheet1", "A1", float64(x), 2, 32) // f.SetCellFloat("Sheet1", "A1", float64(x), 2, 32)
func (f *File) SetCellFloat(sheet, cell string, value float64, precision, bitSize int) error { func (f *File) SetCellFloat(sheet, cell string, value float64, precision, bitSize int) error {
if math.IsNaN(value) || math.IsInf(value, 0) {
return f.SetCellStr(sheet, cell, fmt.Sprint(value))
}
f.mu.Lock()
ws, err := f.workSheetReader(sheet) ws, err := f.workSheetReader(sheet)
if err != nil { if err != nil {
f.mu.Unlock()
return err return err
} }
c, col, row, err := f.prepareCell(ws, cell) f.mu.Unlock()
ws.mu.Lock()
defer ws.mu.Unlock()
c, col, row, err := ws.prepareCell(cell)
if err != nil { if err != nil {
return err return err
} }
ws.Lock() c.S = ws.prepareCellStyle(col, row, c.S)
defer ws.Unlock() c.setCellFloat(value, precision, bitSize)
c.S = f.prepareCellStyle(ws, col, row, c.S)
c.T, c.V = setCellFloat(value, precision, bitSize)
c.IS = nil
return f.removeFormula(c, ws, sheet) return f.removeFormula(c, ws, sheet)
} }
// setCellFloat prepares cell type and string type cell value by a given // setCellFloat prepares cell type and string type cell value by a given float
// float value. // value.
func setCellFloat(value float64, precision, bitSize int) (t string, v string) { func (c *xlsxC) setCellFloat(value float64, precision, bitSize int) {
v = strconv.FormatFloat(value, 'f', precision, bitSize) if math.IsNaN(value) || math.IsInf(value, 0) {
return c.setInlineStr(fmt.Sprint(value))
return
}
c.T, c.V = "", strconv.FormatFloat(value, 'f', precision, bitSize)
c.IS = nil
} }
// SetCellStr provides a function to set string type value of a cell. Total // SetCellStr provides a function to set string type value of a cell. Total
// number of characters that a cell can contain 32767 characters. // number of characters that a cell can contain 32767 characters.
func (f *File) SetCellStr(sheet, cell, value string) error { func (f *File) SetCellStr(sheet, cell, value string) error {
f.mu.Lock()
ws, err := f.workSheetReader(sheet) ws, err := f.workSheetReader(sheet)
if err != nil { if err != nil {
f.mu.Unlock()
return err return err
} }
c, col, row, err := f.prepareCell(ws, cell) f.mu.Unlock()
ws.mu.Lock()
defer ws.mu.Unlock()
c, col, row, err := ws.prepareCell(cell)
if err != nil { if err != nil {
return err return err
} }
ws.Lock() c.S = ws.prepareCellStyle(col, row, c.S)
defer ws.Unlock()
c.S = f.prepareCellStyle(ws, col, row, c.S)
if c.T, c.V, err = f.setCellString(value); err != nil { if c.T, c.V, err = f.setCellString(value); err != nil {
return err return err
} }
@ -394,11 +442,10 @@ func (f *File) SetCellStr(sheet, cell, value string) error {
return f.removeFormula(c, ws, sheet) return f.removeFormula(c, ws, sheet)
} }
// setCellString provides a function to set string type to shared string // setCellString provides a function to set string type to shared string table.
// table.
func (f *File) setCellString(value string) (t, v string, err error) { func (f *File) setCellString(value string) (t, v string, err error) {
if len(value) > TotalCellChars { if utf8.RuneCountInString(value) > TotalCellChars {
value = value[:TotalCellChars] value = string([]rune(value)[:TotalCellChars])
} }
t = "s" t = "s"
var si int var si int
@ -412,8 +459,8 @@ func (f *File) setCellString(value string) (t, v string, err error) {
// sharedStringsLoader load shared string table from system temporary file to // sharedStringsLoader load shared string table from system temporary file to
// memory, and reset shared string table for reader. // memory, and reset shared string table for reader.
func (f *File) sharedStringsLoader() (err error) { func (f *File) sharedStringsLoader() (err error) {
f.Lock() f.mu.Lock()
defer f.Unlock() defer f.mu.Unlock()
if path, ok := f.tempFiles.Load(defaultXMLPathSharedStrings); ok { if path, ok := f.tempFiles.Load(defaultXMLPathSharedStrings); ok {
f.Pkg.Store(defaultXMLPathSharedStrings, f.readBytes(defaultXMLPathSharedStrings)) f.Pkg.Store(defaultXMLPathSharedStrings, f.readBytes(defaultXMLPathSharedStrings))
f.tempFiles.Delete(defaultXMLPathSharedStrings) f.tempFiles.Delete(defaultXMLPathSharedStrings)
@ -442,24 +489,33 @@ func (f *File) setSharedString(val string) (int, error) {
if err != nil { if err != nil {
return 0, err return 0, err
} }
f.Lock() f.mu.Lock()
defer f.Unlock() defer f.mu.Unlock()
if i, ok := f.sharedStringsMap[val]; ok { if i, ok := f.sharedStringsMap[val]; ok {
return i, nil return i, nil
} }
sst.Count++ sst.mu.Lock()
sst.UniqueCount++ defer sst.mu.Unlock()
t := xlsxT{Val: val} t := xlsxT{Val: val}
val, t.Space = trimCellValue(val) val, t.Space = trimCellValue(val, false)
sst.SI = append(sst.SI, xlsxSI{T: &t}) sst.SI = append(sst.SI, xlsxSI{T: &t})
sst.Count = len(sst.SI)
sst.UniqueCount = sst.Count
f.sharedStringsMap[val] = sst.UniqueCount - 1 f.sharedStringsMap[val] = sst.UniqueCount - 1
return sst.UniqueCount - 1, nil return sst.UniqueCount - 1, nil
} }
// trimCellValue provides a function to set string type to cell. // trimCellValue provides a function to set string type to cell.
func trimCellValue(value string) (v string, ns xml.Attr) { func trimCellValue(value string, escape bool) (v string, ns xml.Attr) {
if len(value) > TotalCellChars { if utf8.RuneCountInString(value) > TotalCellChars {
value = value[:TotalCellChars] value = string([]rune(value)[:TotalCellChars])
}
if escape {
var buf bytes.Buffer
enc := xml.NewEncoder(&buf)
_ = enc.EncodeToken(xml.CharData(value))
enc.Flush()
value = buf.String()
} }
if len(value) > 0 { if len(value) > 0 {
prefix, suffix := value[0], value[len(value)-1] prefix, suffix := value[0], value[len(value)-1]
@ -491,18 +547,16 @@ func (c *xlsxC) setCellValue(val string) {
// string. // string.
func (c *xlsxC) setInlineStr(val string) { func (c *xlsxC) setInlineStr(val string) {
c.T, c.V, c.IS = "inlineStr", "", &xlsxSI{T: &xlsxT{}} c.T, c.V, c.IS = "inlineStr", "", &xlsxSI{T: &xlsxT{}}
buf := &bytes.Buffer{} c.IS.T.Val, c.IS.T.Space = trimCellValue(val, true)
_ = xml.EscapeText(buf, []byte(val))
c.IS.T.Val, c.IS.T.Space = trimCellValue(buf.String())
} }
// setStr set cell data type and value which containing a formula string. // setStr set cell data type and value which containing a formula string.
func (c *xlsxC) setStr(val string) { func (c *xlsxC) setStr(val string) {
c.T, c.IS = "str", nil c.T, c.IS = "str", nil
c.V, c.XMLSpace = trimCellValue(val) c.V, c.XMLSpace = trimCellValue(val, false)
} }
// getCellDate parse cell value which containing a boolean. // getCellBool parse cell value which containing a boolean.
func (c *xlsxC) getCellBool(f *File, raw bool) (string, error) { func (c *xlsxC) getCellBool(f *File, raw bool) (string, error) {
if !raw { if !raw {
if c.V == "1" { if c.V == "1" {
@ -512,7 +566,7 @@ func (c *xlsxC) getCellBool(f *File, raw bool) (string, error) {
return "FALSE", nil return "FALSE", nil
} }
} }
return f.formattedValue(c.S, c.V, raw) return f.formattedValue(c, raw, CellTypeBool)
} }
// setCellDefault prepares cell type and string type cell value by a given // setCellDefault prepares cell type and string type cell value by a given
@ -547,15 +601,13 @@ func (c *xlsxC) getCellDate(f *File, raw bool) (string, error) {
c.V = strconv.FormatFloat(excelTime, 'G', 15, 64) c.V = strconv.FormatFloat(excelTime, 'G', 15, 64)
} }
} }
return f.formattedValue(c.S, c.V, raw) return f.formattedValue(c, raw, CellTypeDate)
} }
// getValueFrom return a value from a column/row cell, this function is // getValueFrom return a value from a column/row cell, this function is
// intended to be used with for range on rows an argument with the spreadsheet // intended to be used with for range on rows an argument with the spreadsheet
// opened file. // opened file.
func (c *xlsxC) getValueFrom(f *File, d *xlsxSST, raw bool) (string, error) { func (c *xlsxC) getValueFrom(f *File, d *xlsxSST, raw bool) (string, error) {
f.Lock()
defer f.Unlock()
switch c.T { switch c.T {
case "b": case "b":
return c.getCellBool(f, raw) return c.getCellBool(f, raw)
@ -563,21 +615,24 @@ func (c *xlsxC) getValueFrom(f *File, d *xlsxSST, raw bool) (string, error) {
return c.getCellDate(f, raw) return c.getCellDate(f, raw)
case "s": case "s":
if c.V != "" { if c.V != "" {
xlsxSI := 0 xlsxSI, _ := strconv.Atoi(strings.TrimSpace(c.V))
xlsxSI, _ = strconv.Atoi(strings.TrimSpace(c.V))
if _, ok := f.tempFiles.Load(defaultXMLPathSharedStrings); ok { if _, ok := f.tempFiles.Load(defaultXMLPathSharedStrings); ok {
return f.formattedValue(c.S, f.getFromStringItem(xlsxSI), raw) return f.formattedValue(&xlsxC{S: c.S, V: f.getFromStringItem(xlsxSI)}, raw, CellTypeSharedString)
} }
d.mu.Lock()
defer d.mu.Unlock()
if len(d.SI) > xlsxSI { if len(d.SI) > xlsxSI {
return f.formattedValue(c.S, d.SI[xlsxSI].String(), raw) return f.formattedValue(&xlsxC{S: c.S, V: d.SI[xlsxSI].String()}, raw, CellTypeSharedString)
} }
} }
return f.formattedValue(c.S, c.V, raw) return f.formattedValue(c, raw, CellTypeSharedString)
case "str":
return c.V, nil
case "inlineStr": case "inlineStr":
if c.IS != nil { if c.IS != nil {
return f.formattedValue(c.S, c.IS.String(), raw) return f.formattedValue(&xlsxC{S: c.S, V: c.IS.String()}, raw, CellTypeInlineString)
} }
return f.formattedValue(c.S, c.V, raw) return f.formattedValue(c, raw, CellTypeInlineString)
default: default:
if isNum, precision, decimal := isNumeric(c.V); isNum && !raw { if isNum, precision, decimal := isNumeric(c.V); isNum && !raw {
if precision > 15 { if precision > 15 {
@ -586,24 +641,27 @@ func (c *xlsxC) getValueFrom(f *File, d *xlsxSST, raw bool) (string, error) {
c.V = strconv.FormatFloat(decimal, 'f', -1, 64) c.V = strconv.FormatFloat(decimal, 'f', -1, 64)
} }
} }
return f.formattedValue(c.S, c.V, raw) return f.formattedValue(c, raw, CellTypeNumber)
} }
} }
// SetCellDefault provides a function to set string type value of a cell as // SetCellDefault provides a function to set string type value of a cell as
// default format without escaping the cell. // default format without escaping the cell.
func (f *File) SetCellDefault(sheet, cell, value string) error { func (f *File) SetCellDefault(sheet, cell, value string) error {
f.mu.Lock()
ws, err := f.workSheetReader(sheet) ws, err := f.workSheetReader(sheet)
if err != nil { if err != nil {
f.mu.Unlock()
return err return err
} }
c, col, row, err := f.prepareCell(ws, cell) f.mu.Unlock()
ws.mu.Lock()
defer ws.mu.Unlock()
c, col, row, err := ws.prepareCell(cell)
if err != nil { if err != nil {
return err return err
} }
ws.Lock() c.S = ws.prepareCellStyle(col, row, c.S)
defer ws.Unlock()
c.S = f.prepareCellStyle(ws, col, row, c.S)
c.setCellDefault(value) c.setCellDefault(value)
return f.removeFormula(c, ws, sheet) return f.removeFormula(c, ws, sheet)
} }
@ -611,7 +669,22 @@ func (f *File) SetCellDefault(sheet, cell, value string) error {
// GetCellFormula provides a function to get formula from cell by given // GetCellFormula provides a function to get formula from cell by given
// worksheet name and cell reference in spreadsheet. // worksheet name and cell reference in spreadsheet.
func (f *File) GetCellFormula(sheet, cell string) (string, error) { func (f *File) GetCellFormula(sheet, cell string) (string, error) {
return f.getCellFormula(sheet, cell, false)
}
// getCellFormula provides a function to get transformed formula from cell by
// given worksheet name and cell reference in spreadsheet.
func (f *File) getCellFormula(sheet, cell string, transformed bool) (string, error) {
return f.getCellStringFunc(sheet, cell, func(x *xlsxWorksheet, c *xlsxC) (string, bool, error) { return f.getCellStringFunc(sheet, cell, func(x *xlsxWorksheet, c *xlsxC) (string, bool, error) {
if transformed && !f.formulaChecked {
if err := f.setArrayFormulaCells(); err != nil {
return "", false, err
}
f.formulaChecked = true
}
if transformed && c.f != "" {
return c.f, true, nil
}
if c.F == nil { if c.F == nil {
return "", false, nil return "", false, nil
} }
@ -715,7 +788,7 @@ func (f *File) SetCellFormula(sheet, cell, formula string, opts ...FormulaOpts)
if err != nil { if err != nil {
return err return err
} }
c, _, _, err := f.prepareCell(ws, cell) c, _, _, err := ws.prepareCell(cell)
if err != nil { if err != nil {
return err return err
} }
@ -736,6 +809,11 @@ func (f *File) SetCellFormula(sheet, cell, formula string, opts ...FormulaOpts)
return err return err
} }
c.F.T = *opt.Type c.F.T = *opt.Type
if c.F.T == STCellFormulaTypeArray && opt.Ref != nil {
if err = ws.setArrayFormula(sheet, &xlsxF{Ref: *opt.Ref, Content: formula}, f.GetDefinedName()); err != nil {
return err
}
}
if c.F.T == STCellFormulaTypeShared { if c.F.T == STCellFormulaTypeShared {
if err = ws.setSharedFormula(*opt.Ref); err != nil { if err = ws.setSharedFormula(*opt.Ref); err != nil {
return err return err
@ -750,6 +828,67 @@ func (f *File) SetCellFormula(sheet, cell, formula string, opts ...FormulaOpts)
return err return err
} }
// setArrayFormula transform the array formula in an array formula range to the
// normal formula and set cells in this range to the formula as the normal
// formula.
func (ws *xlsxWorksheet) setArrayFormula(sheet string, formula *xlsxF, definedNames []DefinedName) error {
if len(strings.Split(formula.Ref, ":")) < 2 {
return nil
}
coordinates, err := rangeRefToCoordinates(formula.Ref)
if err != nil {
return err
}
_ = sortCoordinates(coordinates)
tokens, arrayFormulaOperandTokens, err := getArrayFormulaTokens(sheet, formula.Content, definedNames)
if err != nil {
return err
}
topLeftCol, topLeftRow := coordinates[0], coordinates[1]
for c := coordinates[0]; c <= coordinates[2]; c++ {
for r := coordinates[1]; r <= coordinates[3]; r++ {
colOffset, rowOffset := c-topLeftCol, r-topLeftRow
for i, af := range arrayFormulaOperandTokens {
colNum, rowNum := af.topLeftCol+colOffset, af.topLeftRow+rowOffset
if colNum <= af.bottomRightCol && rowNum <= af.bottomRightRow {
arrayFormulaOperandTokens[i].targetCellRef, _ = CoordinatesToCellName(colNum, rowNum)
}
}
ws.prepareSheetXML(c, r)
if cell := &ws.SheetData.Row[r-1].C[c-1]; cell.f == "" {
cell.f = transformArrayFormula(tokens, arrayFormulaOperandTokens)
}
}
}
return err
}
// setArrayFormulaCells transform the array formula in all worksheets to the
// normal formula and set cells in the array formula reference range to the
// formula as the normal formula.
func (f *File) setArrayFormulaCells() error {
definedNames := f.GetDefinedName()
for _, sheetN := range f.GetSheetList() {
ws, err := f.workSheetReader(sheetN)
if err != nil {
if err.Error() == newNotWorksheetError(sheetN).Error() {
continue
}
return err
}
for _, row := range ws.SheetData.Row {
for _, cell := range row.C {
if cell.F != nil && cell.F.T == STCellFormulaTypeArray {
if err = ws.setArrayFormula(sheetN, cell.F, definedNames); err != nil {
return err
}
}
}
}
}
return nil
}
// setSharedFormula set shared formula for the cells. // setSharedFormula set shared formula for the cells.
func (ws *xlsxWorksheet) setSharedFormula(ref string) error { func (ws *xlsxWorksheet) setSharedFormula(ref string) error {
coordinates, err := rangeRefToCoordinates(ref) coordinates, err := rangeRefToCoordinates(ref)
@ -760,7 +899,7 @@ func (ws *xlsxWorksheet) setSharedFormula(ref string) error {
cnt := ws.countSharedFormula() cnt := ws.countSharedFormula()
for c := coordinates[0]; c <= coordinates[2]; c++ { for c := coordinates[0]; c <= coordinates[2]; c++ {
for r := coordinates[1]; r <= coordinates[3]; r++ { for r := coordinates[1]; r <= coordinates[3]; r++ {
prepareSheetXML(ws, c, r) ws.prepareSheetXML(c, r)
cell := &ws.SheetData.Row[r-1].C[c-1] cell := &ws.SheetData.Row[r-1].C[c-1]
if cell.F == nil { if cell.F == nil {
cell.F = &xlsxF{} cell.F = &xlsxF{}
@ -825,14 +964,36 @@ type HyperlinkOpts struct {
Tooltip *string Tooltip *string
} }
// removeHyperLink remove hyperlink for worksheet and delete relationships for
// the worksheet by given sheet name and cell reference. Note that if the cell
// in a range reference, the whole hyperlinks will be deleted.
func (f *File) removeHyperLink(ws *xlsxWorksheet, sheet, cell string) error {
for idx := 0; idx < len(ws.Hyperlinks.Hyperlink); idx++ {
link := ws.Hyperlinks.Hyperlink[idx]
ok, err := f.checkCellInRangeRef(cell, link.Ref)
if err != nil {
return err
}
if link.Ref == cell || ok {
ws.Hyperlinks.Hyperlink = append(ws.Hyperlinks.Hyperlink[:idx], ws.Hyperlinks.Hyperlink[idx+1:]...)
idx--
f.deleteSheetRelationships(sheet, link.RID)
}
}
if len(ws.Hyperlinks.Hyperlink) == 0 {
ws.Hyperlinks = nil
}
return nil
}
// SetCellHyperLink provides a function to set cell hyperlink by given // SetCellHyperLink provides a function to set cell hyperlink by given
// worksheet name and link URL address. LinkType defines two types of // worksheet name and link URL address. LinkType defines three types of
// hyperlink "External" for website or "Location" for moving to one of cell in // hyperlink "External" for website or "Location" for moving to one of cell in
// this workbook. Maximum limit hyperlinks in a worksheet is 65530. This // this workbook or "None" for remove hyperlink. Maximum limit hyperlinks in a
// function is only used to set the hyperlink of the cell and doesn't affect // worksheet is 65530. This function is only used to set the hyperlink of the
// the value of the cell. If you need to set the value of the cell, please use // cell and doesn't affect the value of the cell. If you need to set the value
// the other functions such as `SetCellStyle` or `SetSheetRow`. The below is // of the cell, please use the other functions such as `SetCellStyle` or
// example for external link. // `SetSheetRow`. The below is example for external link.
// //
// display, tooltip := "https://github.com/xuri/excelize", "Excelize on GitHub" // display, tooltip := "https://github.com/xuri/excelize", "Excelize on GitHub"
// if err := f.SetCellHyperLink("Sheet1", "A3", // if err := f.SetCellHyperLink("Sheet1", "A3",
@ -864,7 +1025,7 @@ func (f *File) SetCellHyperLink(sheet, cell, link, linkType string, opts ...Hype
if err != nil { if err != nil {
return err return err
} }
if cell, err = f.mergeCellsParser(ws, cell); err != nil { if cell, err = ws.mergeCellsParser(cell); err != nil {
return err return err
} }
@ -900,8 +1061,10 @@ func (f *File) SetCellHyperLink(sheet, cell, link, linkType string, opts ...Hype
Ref: cell, Ref: cell,
Location: link, Location: link,
} }
case "None":
return f.removeHyperLink(ws, sheet, cell)
default: default:
return fmt.Errorf("invalid link type %q", linkType) return newInvalidLinkTypeError(linkType)
} }
for _, o := range opts { for _, o := range opts {
@ -922,6 +1085,9 @@ func (f *File) SetCellHyperLink(sheet, cell, link, linkType string, opts ...Hype
// getCellRichText returns rich text of cell by given string item. // getCellRichText returns rich text of cell by given string item.
func getCellRichText(si *xlsxSI) (runs []RichTextRun) { func getCellRichText(si *xlsxSI) (runs []RichTextRun) {
if si.T != nil {
runs = append(runs, RichTextRun{Text: si.T.Val})
}
for _, v := range si.R { for _, v := range si.R {
run := RichTextRun{ run := RichTextRun{
Text: v.T.Val, Text: v.T.Val,
@ -941,12 +1107,19 @@ func (f *File) GetCellRichText(sheet, cell string) (runs []RichTextRun, err erro
if err != nil { if err != nil {
return return
} }
c, _, _, err := f.prepareCell(ws, cell) c, _, _, err := ws.prepareCell(cell)
if err != nil { if err != nil {
return return
} }
if c.T == "inlineStr" && c.IS != nil {
runs = getCellRichText(c.IS)
return
}
if c.T != "s" || c.V == "" {
return
}
siIdx, err := strconv.Atoi(c.V) siIdx, err := strconv.Atoi(c.V)
if err != nil || c.T != "s" { if err != nil {
return return
} }
sst, err := f.sharedStringsReader() sst, err := f.sharedStringsReader()
@ -1030,7 +1203,7 @@ func setRichText(runs []RichTextRun) ([]xlsxR, error) {
return textRuns, ErrCellCharsLength return textRuns, ErrCellCharsLength
} }
run := xlsxR{T: &xlsxT{}} run := xlsxR{T: &xlsxT{}}
run.T.Val, run.T.Space = trimCellValue(textRun.Text) run.T.Val, run.T.Space = trimCellValue(textRun.Text, false)
fnt := textRun.Font fnt := textRun.Font
if fnt != nil { if fnt != nil {
run.RPr = newRpr(fnt) run.RPr = newRpr(fnt)
@ -1168,14 +1341,14 @@ func (f *File) SetCellRichText(sheet, cell string, runs []RichTextRun) error {
if err != nil { if err != nil {
return err return err
} }
c, col, row, err := f.prepareCell(ws, cell) c, col, row, err := ws.prepareCell(cell)
if err != nil { if err != nil {
return err return err
} }
if err := f.sharedStringsLoader(); err != nil { if err := f.sharedStringsLoader(); err != nil {
return err return err
} }
c.S = f.prepareCellStyle(ws, col, row, c.S) c.S = ws.prepareCellStyle(col, row, c.S)
si := xlsxSI{} si := xlsxSI{}
sst, err := f.sharedStringsReader() sst, err := f.sharedStringsReader()
if err != nil { if err != nil {
@ -1249,9 +1422,9 @@ func (f *File) setSheetCells(sheet, cell string, slice interface{}, dir adjustDi
} }
// getCellInfo does common preparation for all set cell value functions. // getCellInfo does common preparation for all set cell value functions.
func (f *File) prepareCell(ws *xlsxWorksheet, cell string) (*xlsxC, int, int, error) { func (ws *xlsxWorksheet) prepareCell(cell string) (*xlsxC, int, int, error) {
var err error var err error
cell, err = f.mergeCellsParser(ws, cell) cell, err = ws.mergeCellsParser(cell)
if err != nil { if err != nil {
return nil, 0, 0, err return nil, 0, 0, err
} }
@ -1260,9 +1433,7 @@ func (f *File) prepareCell(ws *xlsxWorksheet, cell string) (*xlsxC, int, int, er
return nil, 0, 0, err return nil, 0, 0, err
} }
prepareSheetXML(ws, col, row) ws.prepareSheetXML(col, row)
ws.Lock()
defer ws.Unlock()
return &ws.SheetData.Row[row-1].C[col-1], col, row, err return &ws.SheetData.Row[row-1].C[col-1], col, row, err
} }
@ -1270,11 +1441,16 @@ func (f *File) prepareCell(ws *xlsxWorksheet, cell string) (*xlsxC, int, int, er
// value function. Passed function implements specific part of required // value function. Passed function implements specific part of required
// logic. // logic.
func (f *File) getCellStringFunc(sheet, cell string, fn func(x *xlsxWorksheet, c *xlsxC) (string, bool, error)) (string, error) { func (f *File) getCellStringFunc(sheet, cell string, fn func(x *xlsxWorksheet, c *xlsxC) (string, bool, error)) (string, error) {
f.mu.Lock()
ws, err := f.workSheetReader(sheet) ws, err := f.workSheetReader(sheet)
if err != nil { if err != nil {
f.mu.Unlock()
return "", err return "", err
} }
cell, err = f.mergeCellsParser(ws, cell) f.mu.Unlock()
ws.mu.Lock()
defer ws.mu.Unlock()
cell, err = ws.mergeCellsParser(cell)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -1282,10 +1458,6 @@ func (f *File) getCellStringFunc(sheet, cell string, fn func(x *xlsxWorksheet, c
if err != nil { if err != nil {
return "", err return "", err
} }
ws.Lock()
defer ws.Unlock()
lastRowNum := 0 lastRowNum := 0
if l := len(ws.SheetData.Row); l > 0 { if l := len(ws.SheetData.Row); l > 0 {
lastRowNum = ws.SheetData.Row[l-1].R lastRowNum = ws.SheetData.Row[l-1].R
@ -1321,52 +1493,60 @@ func (f *File) getCellStringFunc(sheet, cell string, fn func(x *xlsxWorksheet, c
// formattedValue provides a function to returns a value after formatted. If // formattedValue provides a function to returns a value after formatted. If
// it is possible to apply a format to the cell value, it will do so, if not // it is possible to apply a format to the cell value, it will do so, if not
// then an error will be returned, along with the raw value of the cell. // then an error will be returned, along with the raw value of the cell.
func (f *File) formattedValue(s int, v string, raw bool) (string, error) { func (f *File) formattedValue(c *xlsxC, raw bool, cellType CellType) (string, error) {
if raw { if raw || c.S == 0 {
return v, nil return c.V, nil
}
if s == 0 {
return v, nil
} }
styleSheet, err := f.stylesReader() styleSheet, err := f.stylesReader()
if err != nil { if err != nil {
return v, err return c.V, err
} }
if styleSheet.CellXfs == nil { if styleSheet.CellXfs == nil {
return v, err return c.V, err
} }
if s >= len(styleSheet.CellXfs.Xf) || s < 0 { if c.S >= len(styleSheet.CellXfs.Xf) || c.S < 0 {
return v, err return c.V, err
} }
var numFmtID int var numFmtID int
if styleSheet.CellXfs.Xf[s].NumFmtID != nil { if styleSheet.CellXfs.Xf[c.S].NumFmtID != nil {
numFmtID = *styleSheet.CellXfs.Xf[s].NumFmtID numFmtID = *styleSheet.CellXfs.Xf[c.S].NumFmtID
} }
date1904 := false date1904 := false
wb, err := f.workbookReader() wb, err := f.workbookReader()
if err != nil { if err != nil {
return v, err return c.V, err
} }
if wb != nil && wb.WorkbookPr != nil { if wb != nil && wb.WorkbookPr != nil {
date1904 = wb.WorkbookPr.Date1904 date1904 = wb.WorkbookPr.Date1904
} }
if ok := builtInNumFmtFunc[numFmtID]; ok != nil { if fmtCode, ok := styleSheet.getCustomNumFmtCode(numFmtID); ok {
return ok(v, builtInNumFmt[numFmtID], date1904), err return format(c.V, fmtCode, date1904, cellType, f.options), err
} }
if styleSheet.NumFmts == nil { if fmtCode, ok := f.getBuiltInNumFmtCode(numFmtID); ok {
return v, err return f.applyBuiltInNumFmt(c, fmtCode, numFmtID, date1904, cellType), err
} }
for _, xlsxFmt := range styleSheet.NumFmts.NumFmt { return c.V, err
}
// getCustomNumFmtCode provides a function to returns custom number format code.
func (ss *xlsxStyleSheet) getCustomNumFmtCode(numFmtID int) (string, bool) {
if ss.NumFmts == nil {
return "", false
}
for _, xlsxFmt := range ss.NumFmts.NumFmt {
if xlsxFmt.NumFmtID == numFmtID { if xlsxFmt.NumFmtID == numFmtID {
return format(v, xlsxFmt.FormatCode, date1904), err if xlsxFmt.FormatCode16 != "" {
return xlsxFmt.FormatCode16, true
}
return xlsxFmt.FormatCode, true
} }
} }
return v, err return "", false
} }
// prepareCellStyle provides a function to prepare style index of cell in // prepareCellStyle provides a function to prepare style index of cell in
// worksheet by given column index and style index. // worksheet by given column index and style index.
func (f *File) prepareCellStyle(ws *xlsxWorksheet, col, row, style int) int { func (ws *xlsxWorksheet) prepareCellStyle(col, row, style int) int {
if style != 0 { if style != 0 {
return style return style
} }
@ -1387,7 +1567,7 @@ func (f *File) prepareCellStyle(ws *xlsxWorksheet, col, row, style int) int {
// mergeCellsParser provides a function to check merged cells in worksheet by // mergeCellsParser provides a function to check merged cells in worksheet by
// given cell reference. // given cell reference.
func (f *File) mergeCellsParser(ws *xlsxWorksheet, cell string) (string, error) { func (ws *xlsxWorksheet) mergeCellsParser(cell string) (string, error) {
cell = strings.ToUpper(cell) cell = strings.ToUpper(cell)
col, row, err := CellNameToCoordinates(cell) col, row, err := CellNameToCoordinates(cell)
if err != nil { if err != nil {
@ -1508,8 +1688,10 @@ func parseSharedFormula(dCol, dRow int, orig []byte) (res string, start int) {
// Note that this function not validate ref tag to check the cell whether in // Note that this function not validate ref tag to check the cell whether in
// allow range reference, and always return origin shared formula. // allow range reference, and always return origin shared formula.
func getSharedFormula(ws *xlsxWorksheet, si int, cell string) string { func getSharedFormula(ws *xlsxWorksheet, si int, cell string) string {
for _, r := range ws.SheetData.Row { for row := 0; row < len(ws.SheetData.Row); row++ {
for _, c := range r.C { r := &ws.SheetData.Row[row]
for column := 0; column < len(r.C); column++ {
c := &r.C[column]
if c.F != nil && c.F.Ref != "" && c.F.T == STCellFormulaTypeShared && c.F.Si != nil && *c.F.Si == si { if c.F != nil && c.F.Ref != "" && c.F.T == STCellFormulaTypeShared && c.F.Si != nil && *c.F.Si == si {
col, row, _ := CellNameToCoordinates(cell) col, row, _ := CellNameToCoordinates(cell)
sharedCol, sharedRow, _ := CellNameToCoordinates(c.R) sharedCol, sharedRow, _ := CellNameToCoordinates(c.R)

View File

@ -3,6 +3,7 @@ package excelize
import ( import (
"fmt" "fmt"
_ "image/jpeg" _ "image/jpeg"
"math"
"os" "os"
"path/filepath" "path/filepath"
"reflect" "reflect"
@ -41,6 +42,9 @@ func TestConcurrency(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
// Concurrency set cell style // Concurrency set cell style
assert.NoError(t, f.SetCellStyle("Sheet1", "A3", "A3", style)) assert.NoError(t, f.SetCellStyle("Sheet1", "A3", "A3", style))
// Concurrency get cell style
_, err = f.GetCellStyle("Sheet1", "A3")
assert.NoError(t, err)
// Concurrency add picture // Concurrency add picture
assert.NoError(t, f.AddPicture("Sheet1", "F21", filepath.Join("test", "images", "excel.jpg"), assert.NoError(t, f.AddPicture("Sheet1", "F21", filepath.Join("test", "images", "excel.jpg"),
&GraphicOptions{ &GraphicOptions{
@ -65,7 +69,7 @@ func TestConcurrency(t *testing.T) {
// Concurrency iterate columns // Concurrency iterate columns
cols, err := f.Cols("Sheet1") cols, err := f.Cols("Sheet1")
assert.NoError(t, err) assert.NoError(t, err)
for rows.Next() { for cols.Next() {
_, err := cols.Rows() _, err := cols.Rows()
assert.NoError(t, err) assert.NoError(t, err)
} }
@ -87,6 +91,14 @@ func TestConcurrency(t *testing.T) {
visible, err := f.GetColVisible("Sheet1", "A") visible, err := f.GetColVisible("Sheet1", "A")
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, true, visible) assert.Equal(t, true, visible)
// Concurrency add data validation
dv := NewDataValidation(true)
dv.Sqref = fmt.Sprintf("A%d:B%d", val, val)
assert.NoError(t, dv.SetRange(10, 20, DataValidationTypeWhole, DataValidationOperatorGreaterThan))
dv.SetInput(fmt.Sprintf("title:%d", val), strconv.Itoa(val))
assert.NoError(t, f.AddDataValidation("Sheet1", dv))
// Concurrency delete data validation with reference sequence
assert.NoError(t, f.DeleteDataValidation("Sheet1", dv.Sqref))
wg.Done() wg.Done()
}(i, t) }(i, t)
} }
@ -96,6 +108,10 @@ func TestConcurrency(t *testing.T) {
t.Error(err) t.Error(err)
} }
assert.Equal(t, "1", val) assert.Equal(t, "1", val)
// Test the length of data validation
dataValidations, err := f.GetDataValidations("Sheet1")
assert.NoError(t, err)
assert.Len(t, dataValidations, 0)
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestConcurrency.xlsx"))) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestConcurrency.xlsx")))
assert.NoError(t, f.Close()) assert.NoError(t, f.Close())
} }
@ -133,11 +149,11 @@ func TestCheckCellInRangeRef(t *testing.T) {
} }
ok, err := f.checkCellInRangeRef("A1", "A:B") ok, err := f.checkCellInRangeRef("A1", "A:B")
assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), err)
assert.False(t, ok) assert.False(t, ok)
ok, err = f.checkCellInRangeRef("AA0", "Z0:AB1") ok, err = f.checkCellInRangeRef("AA0", "Z0:AB1")
assert.EqualError(t, err, newCellNameToCoordinatesError("AA0", newInvalidCellNameError("AA0")).Error()) assert.Equal(t, newCellNameToCoordinatesError("AA0", newInvalidCellNameError("AA0")), err)
assert.False(t, ok) assert.False(t, ok)
} }
@ -171,15 +187,68 @@ func TestSetCellFloat(t *testing.T) {
assert.Equal(t, "123.42", val, "A1 should be 123.42") assert.Equal(t, "123.42", val, "A1 should be 123.42")
}) })
f := NewFile() f := NewFile()
assert.EqualError(t, f.SetCellFloat(sheet, "A", 123.42, -1, 64), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.SetCellFloat(sheet, "A", 123.42, -1, 64))
// Test set cell float data type value with invalid sheet name // Test set cell float data type value with invalid sheet name
assert.EqualError(t, f.SetCellFloat("Sheet:1", "A1", 123.42, -1, 64), ErrSheetNameInvalid.Error()) assert.Equal(t, ErrSheetNameInvalid, f.SetCellFloat("Sheet:1", "A1", 123.42, -1, 64))
}
func TestSetCellUint(t *testing.T) {
f := NewFile()
assert.NoError(t, f.SetCellValue("Sheet1", "A1", uint8(math.MaxUint8)))
result, err := f.GetCellValue("Sheet1", "A1")
assert.Equal(t, "255", result)
assert.NoError(t, err)
assert.NoError(t, f.SetCellValue("Sheet1", "A1", uint(math.MaxUint16)))
result, err = f.GetCellValue("Sheet1", "A1")
assert.Equal(t, "65535", result)
assert.NoError(t, err)
assert.NoError(t, f.SetCellValue("Sheet1", "A1", uint(math.MaxUint32)))
result, err = f.GetCellValue("Sheet1", "A1")
assert.Equal(t, "4294967295", result)
assert.NoError(t, err)
// Test uint cell value not exists worksheet
assert.EqualError(t, f.SetCellUint("SheetN", "A1", 1), "sheet SheetN does not exist")
// Test uint cell value with illegal cell reference
assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.SetCellUint("Sheet1", "A", 1))
}
func TestSetCellValuesMultiByte(t *testing.T) {
f := NewFile()
row := []interface{}{
// Test set cell value with multi byte characters value
strings.Repeat("\u4E00", TotalCellChars+1),
// Test set cell value with XML escape characters
strings.Repeat("<>", TotalCellChars/2),
strings.Repeat(">", TotalCellChars-1),
strings.Repeat(">", TotalCellChars+1),
}
assert.NoError(t, f.SetSheetRow("Sheet1", "A1", &row))
// Test set cell value with XML escape characters in stream writer
_, err := f.NewSheet("Sheet2")
assert.NoError(t, err)
streamWriter, err := f.NewStreamWriter("Sheet2")
assert.NoError(t, err)
assert.NoError(t, streamWriter.SetRow("A1", row))
assert.NoError(t, streamWriter.Flush())
for _, sheetName := range []string{"Sheet1", "Sheet2"} {
for cell, expected := range map[string]int{
"A1": TotalCellChars,
"B1": TotalCellChars - 1,
"C1": TotalCellChars - 1,
"D1": TotalCellChars,
} {
result, err := f.GetCellValue(sheetName, cell)
assert.NoError(t, err)
assert.Len(t, []rune(result), expected)
}
}
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellValuesMultiByte.xlsx")))
} }
func TestSetCellValue(t *testing.T) { func TestSetCellValue(t *testing.T) {
f := NewFile() f := NewFile()
assert.EqualError(t, f.SetCellValue("Sheet1", "A", time.Now().UTC()), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.SetCellValue("Sheet1", "A", time.Now().UTC()))
assert.EqualError(t, f.SetCellValue("Sheet1", "A", time.Duration(1e13)), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.SetCellValue("Sheet1", "A", time.Duration(1e13)))
// Test set cell value with column and row style inherit // Test set cell value with column and row style inherit
style1, err := f.NewStyle(&Style{NumFmt: 2}) style1, err := f.NewStyle(&Style{NumFmt: 2})
assert.NoError(t, err) assert.NoError(t, err)
@ -197,7 +266,7 @@ func TestSetCellValue(t *testing.T) {
assert.Equal(t, "0.50", B2) assert.Equal(t, "0.50", B2)
// Test set cell value with invalid sheet name // Test set cell value with invalid sheet name
assert.EqualError(t, f.SetCellValue("Sheet:1", "A1", "A1"), ErrSheetNameInvalid.Error()) assert.Equal(t, ErrSheetNameInvalid, f.SetCellValue("Sheet:1", "A1", "A1"))
// Test set cell value with unsupported charset shared strings table // Test set cell value with unsupported charset shared strings table
f.SharedStrings = nil f.SharedStrings = nil
f.Pkg.Store(defaultXMLPathSharedStrings, MacintoshCyrillicCharset) f.Pkg.Store(defaultXMLPathSharedStrings, MacintoshCyrillicCharset)
@ -206,6 +275,59 @@ func TestSetCellValue(t *testing.T) {
f.WorkBook = nil f.WorkBook = nil
f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset) f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset)
assert.EqualError(t, f.SetCellValue("Sheet1", "A1", time.Now().UTC()), "XML syntax error on line 1: invalid UTF-8") assert.EqualError(t, f.SetCellValue("Sheet1", "A1", time.Now().UTC()), "XML syntax error on line 1: invalid UTF-8")
// Test set cell value with the shared string table's count not equal with unique count
f = NewFile()
f.SharedStrings = nil
f.Pkg.Store(defaultXMLPathSharedStrings, []byte(fmt.Sprintf(`<sst xmlns="%s" count="2" uniqueCount="1"><si><t>a</t></si><si><t>a</t></si></sst>`, NameSpaceSpreadSheet.Value)))
f.Sheet.Store("xl/worksheets/sheet1.xml", &xlsxWorksheet{
SheetData: xlsxSheetData{Row: []xlsxRow{
{R: 1, C: []xlsxC{{R: "A1", T: "str", V: "1"}}},
}},
})
assert.NoError(t, f.SetCellValue("Sheet1", "A1", "b"))
val, err := f.GetCellValue("Sheet1", "A1")
assert.NoError(t, err)
assert.Equal(t, "b", val)
assert.NoError(t, f.SetCellValue("Sheet1", "B1", "b"))
val, err = f.GetCellValue("Sheet1", "B1")
assert.NoError(t, err)
assert.Equal(t, "b", val)
f = NewFile()
// Test set cell value with an IEEE 754 "not-a-number" value or infinity
for num, expected := range map[float64]string{
math.NaN(): "NaN",
math.Inf(0): "+Inf",
math.Inf(-1): "-Inf",
} {
assert.NoError(t, f.SetCellValue("Sheet1", "A1", num))
val, err := f.GetCellValue("Sheet1", "A1")
assert.NoError(t, err)
assert.Equal(t, expected, val)
}
// Test set cell value with time duration
for val, expected := range map[time.Duration]string{
time.Hour*21 + time.Minute*51 + time.Second*44: "21:51:44",
time.Hour*21 + time.Minute*50: "21:50",
time.Hour*24 + time.Minute*51 + time.Second*44: "24:51:44",
time.Hour*24 + time.Minute*50: "24:50:00",
} {
assert.NoError(t, f.SetCellValue("Sheet1", "A1", val))
val, err := f.GetCellValue("Sheet1", "A1")
assert.NoError(t, err)
assert.Equal(t, expected, val)
}
// Test set cell value with time
for val, expected := range map[time.Time]string{
time.Date(2024, time.October, 1, 0, 0, 0, 0, time.UTC): "Oct-24",
time.Date(2024, time.October, 10, 0, 0, 0, 0, time.UTC): "10-10-24",
time.Date(2024, time.October, 10, 12, 0, 0, 0, time.UTC): "10/10/24 12:00",
} {
assert.NoError(t, f.SetCellValue("Sheet1", "A1", val))
val, err := f.GetCellValue("Sheet1", "A1")
assert.NoError(t, err)
assert.Equal(t, expected, val)
}
} }
func TestSetCellValues(t *testing.T) { func TestSetCellValues(t *testing.T) {
@ -215,7 +337,7 @@ func TestSetCellValues(t *testing.T) {
v, err := f.GetCellValue("Sheet1", "A1") v, err := f.GetCellValue("Sheet1", "A1")
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, v, "12/31/10 00:00") assert.Equal(t, v, "12-31-10")
// Test date value lower than min date supported by Excel // Test date value lower than min date supported by Excel
err = f.SetCellValue("Sheet1", "A1", time.Date(1600, time.December, 31, 0, 0, 0, 0, time.UTC)) err = f.SetCellValue("Sheet1", "A1", time.Date(1600, time.December, 31, 0, 0, 0, 0, time.UTC))
@ -228,9 +350,9 @@ func TestSetCellValues(t *testing.T) {
func TestSetCellBool(t *testing.T) { func TestSetCellBool(t *testing.T) {
f := NewFile() f := NewFile()
assert.EqualError(t, f.SetCellBool("Sheet1", "A", true), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.SetCellBool("Sheet1", "A", true))
// Test set cell boolean data type value with invalid sheet name // Test set cell boolean data type value with invalid sheet name
assert.EqualError(t, f.SetCellBool("Sheet:1", "A1", true), ErrSheetNameInvalid.Error()) assert.Equal(t, ErrSheetNameInvalid, f.SetCellBool("Sheet:1", "A1", true))
} }
func TestSetCellTime(t *testing.T) { func TestSetCellTime(t *testing.T) {
@ -259,7 +381,7 @@ func TestGetCellValue(t *testing.T) {
f.Sheet.Delete("xl/worksheets/sheet1.xml") f.Sheet.Delete("xl/worksheets/sheet1.xml")
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `<row r="3"><c t="inlineStr"><is><t>A3</t></is></c></row><row><c t="inlineStr"><is><t>A4</t></is></c><c t="inlineStr"><is><t>B4</t></is></c></row><row r="7"><c t="inlineStr"><is><t>A7</t></is></c><c t="inlineStr"><is><t>B7</t></is></c></row><row><c t="inlineStr"><is><t>A8</t></is></c><c t="inlineStr"><is><t>B8</t></is></c></row>`))) f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `<row r="3"><c t="inlineStr"><is><t>A3</t></is></c></row><row><c t="inlineStr"><is><t>A4</t></is></c><c t="inlineStr"><is><t>B4</t></is></c></row><row r="7"><c t="inlineStr"><is><t>A7</t></is></c><c t="inlineStr"><is><t>B7</t></is></c></row><row><c t="inlineStr"><is><t>A8</t></is></c><c t="inlineStr"><is><t>B8</t></is></c></row>`)))
f.checked = nil f.checked = sync.Map{}
cells := []string{"A3", "A4", "B4", "A7", "B7"} cells := []string{"A3", "A4", "B4", "A7", "B7"}
rows, err := f.GetRows("Sheet1") rows, err := f.GetRows("Sheet1")
assert.Equal(t, [][]string{nil, nil, {"A3"}, {"A4", "B4"}, nil, nil, {"A7", "B7"}, {"A8", "B8"}}, rows) assert.Equal(t, [][]string{nil, nil, {"A3"}, {"A4", "B4"}, nil, nil, {"A7", "B7"}, {"A8", "B8"}}, rows)
@ -275,35 +397,35 @@ func TestGetCellValue(t *testing.T) {
f.Sheet.Delete("xl/worksheets/sheet1.xml") f.Sheet.Delete("xl/worksheets/sheet1.xml")
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `<row r="2"><c r="A2" t="inlineStr"><is><t>A2</t></is></c></row><row r="2"><c r="B2" t="inlineStr"><is><t>B2</t></is></c></row>`))) f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `<row r="2"><c r="A2" t="inlineStr"><is><t>A2</t></is></c></row><row r="2"><c r="B2" t="inlineStr"><is><t>B2</t></is></c></row>`)))
f.checked = nil f.checked = sync.Map{}
cell, err := f.GetCellValue("Sheet1", "A2") cell, err := f.GetCellValue("Sheet1", "A2")
assert.Equal(t, "A2", cell) assert.Equal(t, "A2", cell)
assert.NoError(t, err) assert.NoError(t, err)
f.Sheet.Delete("xl/worksheets/sheet1.xml") f.Sheet.Delete("xl/worksheets/sheet1.xml")
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `<row r="2"><c r="A2" t="inlineStr"><is><t>A2</t></is></c></row><row r="2"><c r="B2" t="inlineStr"><is><t>B2</t></is></c></row>`))) f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `<row r="2"><c r="A2" t="inlineStr"><is><t>A2</t></is></c></row><row r="2"><c r="B2" t="inlineStr"><is><t>B2</t></is></c></row>`)))
f.checked = nil f.checked = sync.Map{}
rows, err = f.GetRows("Sheet1") rows, err = f.GetRows("Sheet1")
assert.Equal(t, [][]string{nil, {"A2", "B2"}}, rows) assert.Equal(t, [][]string{nil, {"A2", "B2"}}, rows)
assert.NoError(t, err) assert.NoError(t, err)
f.Sheet.Delete("xl/worksheets/sheet1.xml") f.Sheet.Delete("xl/worksheets/sheet1.xml")
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `<row r="1"><c r="A1" t="inlineStr"><is><t>A1</t></is></c></row><row r="1"><c r="B1" t="inlineStr"><is><t>B1</t></is></c></row>`))) f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `<row r="1"><c r="A1" t="inlineStr"><is><t>A1</t></is></c></row><row r="1"><c r="B1" t="inlineStr"><is><t>B1</t></is></c></row>`)))
f.checked = nil f.checked = sync.Map{}
rows, err = f.GetRows("Sheet1") rows, err = f.GetRows("Sheet1")
assert.Equal(t, [][]string{{"A1", "B1"}}, rows) assert.Equal(t, [][]string{{"A1", "B1"}}, rows)
assert.NoError(t, err) assert.NoError(t, err)
f.Sheet.Delete("xl/worksheets/sheet1.xml") f.Sheet.Delete("xl/worksheets/sheet1.xml")
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `<row><c t="inlineStr"><is><t>A3</t></is></c></row><row><c t="inlineStr"><is><t>A4</t></is></c><c t="inlineStr"><is><t>B4</t></is></c></row><row r="7"><c t="inlineStr"><is><t>A7</t></is></c><c t="inlineStr"><is><t>B7</t></is></c></row><row><c t="inlineStr"><is><t>A8</t></is></c><c t="inlineStr"><is><t>B8</t></is></c></row>`))) f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `<row><c t="inlineStr"><is><t>A3</t></is></c></row><row><c t="inlineStr"><is><t>A4</t></is></c><c t="inlineStr"><is><t>B4</t></is></c></row><row r="7"><c t="inlineStr"><is><t>A7</t></is></c><c t="inlineStr"><is><t>B7</t></is></c></row><row><c t="inlineStr"><is><t>A8</t></is></c><c t="inlineStr"><is><t>B8</t></is></c></row>`)))
f.checked = nil f.checked = sync.Map{}
rows, err = f.GetRows("Sheet1") rows, err = f.GetRows("Sheet1")
assert.Equal(t, [][]string{{"A3"}, {"A4", "B4"}, nil, nil, nil, nil, {"A7", "B7"}, {"A8", "B8"}}, rows) assert.Equal(t, [][]string{{"A3"}, {"A4", "B4"}, nil, nil, nil, nil, {"A7", "B7"}, {"A8", "B8"}}, rows)
assert.NoError(t, err) assert.NoError(t, err)
f.Sheet.Delete("xl/worksheets/sheet1.xml") f.Sheet.Delete("xl/worksheets/sheet1.xml")
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `<row r="0"><c r="H6" t="inlineStr"><is><t>H6</t></is></c><c r="A1" t="inlineStr"><is><t>r0A6</t></is></c><c r="F4" t="inlineStr"><is><t>F4</t></is></c></row><row><c r="A1" t="inlineStr"><is><t>A6</t></is></c><c r="B1" t="inlineStr"><is><t>B6</t></is></c><c r="C1" t="inlineStr"><is><t>C6</t></is></c></row><row r="3"><c r="A3"><v>100</v></c><c r="B3" t="inlineStr"><is><t>B3</t></is></c></row>`))) f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `<row r="0"><c r="H6" t="inlineStr"><is><t>H6</t></is></c><c r="A1" t="inlineStr"><is><t>r0A6</t></is></c><c r="F4" t="inlineStr"><is><t>F4</t></is></c></row><row><c r="A1" t="inlineStr"><is><t>A6</t></is></c><c r="B1" t="inlineStr"><is><t>B6</t></is></c><c r="C1" t="inlineStr"><is><t>C6</t></is></c></row><row r="3"><c r="A3"><v>100</v></c><c r="B3" t="inlineStr"><is><t>B3</t></is></c></row>`)))
f.checked = nil f.checked = sync.Map{}
cell, err = f.GetCellValue("Sheet1", "H6") cell, err = f.GetCellValue("Sheet1", "H6")
assert.Equal(t, "H6", cell) assert.Equal(t, "H6", cell)
assert.NoError(t, err) assert.NoError(t, err)
@ -318,6 +440,16 @@ func TestGetCellValue(t *testing.T) {
}, rows) }, rows)
assert.NoError(t, err) assert.NoError(t, err)
f.Sheet.Delete("xl/worksheets/sheet1.xml")
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `<row><c r="A1" t="inlineStr"><is><t>A1</t></is></c></row><row></row><row><c r="A3" t="inlineStr"><is><t>A3</t></is></c></row>`)))
f.checked = sync.Map{}
rows, err = f.GetRows("Sheet1")
assert.Equal(t, [][]string{{"A1"}, nil, {"A3"}}, rows)
assert.NoError(t, err)
cell, err = f.GetCellValue("Sheet1", "A3")
assert.Equal(t, "A3", cell)
assert.NoError(t, err)
f.Sheet.Delete("xl/worksheets/sheet1.xml") f.Sheet.Delete("xl/worksheets/sheet1.xml")
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, ` f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `
<row r="1"><c r="A1"><v>2422.3000000000002</v></c></row> <row r="1"><c r="A1"><v>2422.3000000000002</v></c></row>
@ -356,7 +488,7 @@ func TestGetCellValue(t *testing.T) {
<row r="34"><c r="A34" t="d"><v>20221022T150529Z</v></c></row> <row r="34"><c r="A34" t="d"><v>20221022T150529Z</v></c></row>
<row r="35"><c r="A35" t="d"><v>2022-10-22T15:05:29Z</v></c></row> <row r="35"><c r="A35" t="d"><v>2022-10-22T15:05:29Z</v></c></row>
<row r="36"><c r="A36" t="d"><v>2020-07-10 15:00:00.000</v></c></row>`))) <row r="36"><c r="A36" t="d"><v>2020-07-10 15:00:00.000</v></c></row>`)))
f.checked = nil f.checked = sync.Map{}
rows, err = f.GetCols("Sheet1") rows, err = f.GetCols("Sheet1")
assert.Equal(t, []string{ assert.Equal(t, []string{
"2422.3", "2422.3",
@ -405,7 +537,7 @@ func TestGetCellValue(t *testing.T) {
assert.EqualError(t, value, "XML syntax error on line 1: invalid UTF-8") assert.EqualError(t, value, "XML syntax error on line 1: invalid UTF-8")
// Test get cell value with invalid sheet name // Test get cell value with invalid sheet name
_, err = f.GetCellValue("Sheet:1", "A1") _, err = f.GetCellValue("Sheet:1", "A1")
assert.EqualError(t, err, ErrSheetNameInvalid.Error()) assert.Equal(t, ErrSheetNameInvalid, err)
} }
func TestGetCellType(t *testing.T) { func TestGetCellType(t *testing.T) {
@ -418,10 +550,10 @@ func TestGetCellType(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, CellTypeSharedString, cellType) assert.Equal(t, CellTypeSharedString, cellType)
_, err = f.GetCellType("Sheet1", "A") _, err = f.GetCellType("Sheet1", "A")
assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), err)
// Test get cell type with invalid sheet name // Test get cell type with invalid sheet name
_, err = f.GetCellType("Sheet:1", "A1") _, err = f.GetCellType("Sheet:1", "A1")
assert.EqualError(t, err, ErrSheetNameInvalid.Error()) assert.Equal(t, ErrSheetNameInvalid, err)
} }
func TestGetValueFrom(t *testing.T) { func TestGetValueFrom(t *testing.T) {
@ -447,7 +579,7 @@ func TestGetCellFormula(t *testing.T) {
// Test get cell formula with invalid sheet name // Test get cell formula with invalid sheet name
_, err = f.GetCellFormula("Sheet:1", "A1") _, err = f.GetCellFormula("Sheet:1", "A1")
assert.EqualError(t, err, ErrSheetNameInvalid.Error()) assert.Equal(t, ErrSheetNameInvalid, err)
// Test get cell formula on no formula cell // Test get cell formula on no formula cell
assert.NoError(t, f.SetCellValue("Sheet1", "A1", true)) assert.NoError(t, f.SetCellValue("Sheet1", "A1", true))
@ -475,6 +607,25 @@ func TestGetCellFormula(t *testing.T) {
formula, err := f.GetCellFormula("Sheet1", "B2") formula, err := f.GetCellFormula("Sheet1", "B2")
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "", formula) assert.Equal(t, "", formula)
// Test get array formula with invalid cell range reference
f = NewFile()
assert.NoError(t, f.AddChartSheet("Chart1", &Chart{Type: Line}))
_, err = f.NewSheet("Sheet2")
assert.NoError(t, err)
formulaType, ref := STCellFormulaTypeArray, "B1:B2"
assert.NoError(t, f.SetCellFormula("Sheet2", "B1", "A1:B2", FormulaOpts{Ref: &ref, Type: &formulaType}))
ws, ok := f.Sheet.Load("xl/worksheets/sheet3.xml")
assert.True(t, ok)
ws.(*xlsxWorksheet).SheetData.Row[0].C[1].F.Ref = ":"
_, err = f.getCellFormula("Sheet2", "A1", true)
assert.Equal(t, newCellNameToCoordinatesError("", newInvalidCellNameError("")), err)
// Test set formula for the cells in array formula range with unsupported charset
f = NewFile()
f.Sheet.Delete("xl/worksheets/sheet1.xml")
f.Pkg.Store("xl/worksheets/sheet1.xml", MacintoshCyrillicCharset)
assert.EqualError(t, f.setArrayFormulaCells(), "XML syntax error on line 1: invalid UTF-8")
} }
func ExampleFile_SetCellFloat() { func ExampleFile_SetCellFloat() {
@ -533,10 +684,10 @@ func TestSetCellFormula(t *testing.T) {
assert.NoError(t, f.SetCellFormula("Sheet1", "C19", "SUM(Sheet2!D2,Sheet2!D9)")) assert.NoError(t, f.SetCellFormula("Sheet1", "C19", "SUM(Sheet2!D2,Sheet2!D9)"))
// Test set cell formula with invalid sheet name // Test set cell formula with invalid sheet name
assert.EqualError(t, f.SetCellFormula("Sheet:1", "A1", "SUM(1,2)"), ErrSheetNameInvalid.Error()) assert.Equal(t, ErrSheetNameInvalid, f.SetCellFormula("Sheet:1", "A1", "SUM(1,2)"))
// Test set cell formula with illegal rows number // Test set cell formula with illegal rows number
assert.EqualError(t, f.SetCellFormula("Sheet1", "C", "SUM(Sheet2!D2,Sheet2!D9)"), newCellNameToCoordinatesError("C", newInvalidCellNameError("C")).Error()) assert.Equal(t, newCellNameToCoordinatesError("C", newInvalidCellNameError("C")), f.SetCellFormula("Sheet1", "C", "SUM(Sheet2!D2,Sheet2!D9)"))
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellFormula1.xlsx"))) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellFormula1.xlsx")))
assert.NoError(t, f.Close()) assert.NoError(t, f.Close())
@ -568,7 +719,7 @@ func TestSetCellFormula(t *testing.T) {
ref = "D1:D5" ref = "D1:D5"
assert.NoError(t, f.SetCellFormula("Sheet1", "D1", "=A1+C1", FormulaOpts{Ref: &ref, Type: &formulaType})) assert.NoError(t, f.SetCellFormula("Sheet1", "D1", "=A1+C1", FormulaOpts{Ref: &ref, Type: &formulaType}))
ref = "" ref = ""
assert.EqualError(t, f.SetCellFormula("Sheet1", "D1", "=A1+C1", FormulaOpts{Ref: &ref, Type: &formulaType}), ErrParameterInvalid.Error()) assert.Equal(t, ErrParameterInvalid, f.SetCellFormula("Sheet1", "D1", "=A1+C1", FormulaOpts{Ref: &ref, Type: &formulaType}))
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellFormula5.xlsx"))) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellFormula5.xlsx")))
// Test set table formula for the cells // Test set table formula for the cells
@ -580,6 +731,14 @@ func TestSetCellFormula(t *testing.T) {
formulaType = STCellFormulaTypeDataTable formulaType = STCellFormulaTypeDataTable
assert.NoError(t, f.SetCellFormula("Sheet1", "C2", "=SUM(Table1[[A]:[B]])", FormulaOpts{Type: &formulaType})) assert.NoError(t, f.SetCellFormula("Sheet1", "C2", "=SUM(Table1[[A]:[B]])", FormulaOpts{Type: &formulaType}))
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellFormula6.xlsx"))) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellFormula6.xlsx")))
// Test set array formula with invalid cell range reference
formulaType, ref = STCellFormulaTypeArray, ":"
assert.Equal(t, newCellNameToCoordinatesError("", newInvalidCellNameError("")), f.SetCellFormula("Sheet1", "B1", "A1:A2", FormulaOpts{Ref: &ref, Type: &formulaType}))
// Test set array formula with invalid cell reference
formulaType, ref = STCellFormulaTypeArray, "A1:A2"
assert.Equal(t, ErrColumnNumber, f.SetCellFormula("Sheet1", "A1", "SUM(XFE1:XFE2)", FormulaOpts{Ref: &ref, Type: &formulaType}))
} }
func TestGetCellRichText(t *testing.T) { func TestGetCellRichText(t *testing.T) {
@ -621,32 +780,54 @@ func TestGetCellRichText(t *testing.T) {
runsSource[1].Font.Color = strings.ToUpper(runsSource[1].Font.Color) runsSource[1].Font.Color = strings.ToUpper(runsSource[1].Font.Color)
assert.True(t, reflect.DeepEqual(runsSource[1].Font, runs[1].Font), "should get the same font") assert.True(t, reflect.DeepEqual(runsSource[1].Font, runs[1].Font), "should get the same font")
// Test get cell rich text when string item index overflow // Test get cell rich text with inlineStr
ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml") ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
assert.True(t, ok) assert.True(t, ok)
ws.(*xlsxWorksheet).SheetData.Row[0].C[0].V = "2" ws.(*xlsxWorksheet).SheetData.Row[0].C[0] = xlsxC{
T: "inlineStr",
IS: &xlsxSI{
T: &xlsxT{Val: "A"},
R: []xlsxR{{T: &xlsxT{Val: "1"}}},
},
}
runs, err = f.GetCellRichText("Sheet1", "A1")
assert.NoError(t, err)
assert.Equal(t, []RichTextRun{{Text: "A"}, {Text: "1"}}, runs)
// Test get cell rich text when string item index overflow
ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml")
assert.True(t, ok)
ws.(*xlsxWorksheet).SheetData.Row[0].C[0] = xlsxC{V: "2", IS: &xlsxSI{}}
runs, err = f.GetCellRichText("Sheet1", "A1") runs, err = f.GetCellRichText("Sheet1", "A1")
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 0, len(runs)) assert.Equal(t, 0, len(runs))
// Test get cell rich text when string item index is negative // Test get cell rich text when string item index is negative
ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml") ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml")
assert.True(t, ok) assert.True(t, ok)
ws.(*xlsxWorksheet).SheetData.Row[0].C[0].V = "-1" ws.(*xlsxWorksheet).SheetData.Row[0].C[0] = xlsxC{T: "s", V: "-1", IS: &xlsxSI{}}
runs, err = f.GetCellRichText("Sheet1", "A1") runs, err = f.GetCellRichText("Sheet1", "A1")
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 0, len(runs)) assert.Equal(t, 0, len(runs))
// Test get cell rich text when string item index is invalid
ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml")
assert.True(t, ok)
ws.(*xlsxWorksheet).SheetData.Row[0].C[0] = xlsxC{T: "s", V: "A", IS: &xlsxSI{}}
runs, err = f.GetCellRichText("Sheet1", "A1")
assert.EqualError(t, err, "strconv.Atoi: parsing \"A\": invalid syntax")
assert.Equal(t, 0, len(runs))
// Test get cell rich text on invalid string item index // Test get cell rich text on invalid string item index
ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml") ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml")
assert.True(t, ok) assert.True(t, ok)
ws.(*xlsxWorksheet).SheetData.Row[0].C[0].V = "x" ws.(*xlsxWorksheet).SheetData.Row[0].C[0] = xlsxC{V: "x"}
_, err = f.GetCellRichText("Sheet1", "A1") runs, err = f.GetCellRichText("Sheet1", "A1")
assert.EqualError(t, err, "strconv.Atoi: parsing \"x\": invalid syntax") assert.NoError(t, err)
assert.Equal(t, 0, len(runs))
// Test set cell rich text on not exists worksheet // Test set cell rich text on not exists worksheet
_, err = f.GetCellRichText("SheetN", "A1") _, err = f.GetCellRichText("SheetN", "A1")
assert.EqualError(t, err, "sheet SheetN does not exist") assert.EqualError(t, err, "sheet SheetN does not exist")
// Test set cell rich text with illegal cell reference // Test set cell rich text with illegal cell reference
_, err = f.GetCellRichText("Sheet1", "A") _, err = f.GetCellRichText("Sheet1", "A")
assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), err)
// Test set rich text color theme without tint // Test set rich text color theme without tint
assert.NoError(t, f.SetCellRichText("Sheet1", "A1", []RichTextRun{{Font: &Font{ColorTheme: &theme}}})) assert.NoError(t, f.SetCellRichText("Sheet1", "A1", []RichTextRun{{Font: &Font{ColorTheme: &theme}}}))
// Test set rich text color tint without theme // Test set rich text color tint without theme
@ -663,7 +844,7 @@ func TestGetCellRichText(t *testing.T) {
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
// Test get cell rich text with invalid sheet name // Test get cell rich text with invalid sheet name
_, err = f.GetCellRichText("Sheet:1", "A1") _, err = f.GetCellRichText("Sheet:1", "A1")
assert.EqualError(t, err, ErrSheetNameInvalid.Error()) assert.Equal(t, ErrSheetNameInvalid, err)
} }
func TestSetCellRichText(t *testing.T) { func TestSetCellRichText(t *testing.T) {
@ -762,7 +943,7 @@ func TestSetCellRichText(t *testing.T) {
// Test set cell rich text with invalid sheet name // Test set cell rich text with invalid sheet name
assert.EqualError(t, f.SetCellRichText("Sheet:1", "A1", richTextRun), ErrSheetNameInvalid.Error()) assert.EqualError(t, f.SetCellRichText("Sheet:1", "A1", richTextRun), ErrSheetNameInvalid.Error())
// Test set cell rich text with illegal cell reference // Test set cell rich text with illegal cell reference
assert.EqualError(t, f.SetCellRichText("Sheet1", "A", richTextRun), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.SetCellRichText("Sheet1", "A", richTextRun))
richTextRun = []RichTextRun{{Text: strings.Repeat("s", TotalCellChars+1)}} richTextRun = []RichTextRun{{Text: strings.Repeat("s", TotalCellChars+1)}}
// Test set cell rich text with characters over the maximum limit // Test set cell rich text with characters over the maximum limit
assert.EqualError(t, f.SetCellRichText("Sheet1", "A1", richTextRun), ErrCellCharsLength.Error()) assert.EqualError(t, f.SetCellRichText("Sheet1", "A1", richTextRun), ErrCellCharsLength.Error())
@ -770,21 +951,21 @@ func TestSetCellRichText(t *testing.T) {
func TestFormattedValue(t *testing.T) { func TestFormattedValue(t *testing.T) {
f := NewFile() f := NewFile()
result, err := f.formattedValue(0, "43528", false) result, err := f.formattedValue(&xlsxC{S: 0, V: "43528"}, false, CellTypeNumber)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "43528", result) assert.Equal(t, "43528", result)
// S is too large // S is too large
result, err = f.formattedValue(15, "43528", false) result, err = f.formattedValue(&xlsxC{S: 15, V: "43528"}, false, CellTypeNumber)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "43528", result) assert.Equal(t, "43528", result)
// S is too small // S is too small
result, err = f.formattedValue(-15, "43528", false) result, err = f.formattedValue(&xlsxC{S: -15, V: "43528"}, false, CellTypeNumber)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "43528", result) assert.Equal(t, "43528", result)
result, err = f.formattedValue(1, "43528", false) result, err = f.formattedValue(&xlsxC{S: 1, V: "43528"}, false, CellTypeNumber)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "43528", result) assert.Equal(t, "43528", result)
customNumFmt := "[$-409]MM/DD/YYYY" customNumFmt := "[$-409]MM/DD/YYYY"
@ -792,7 +973,7 @@ func TestFormattedValue(t *testing.T) {
CustomNumFmt: &customNumFmt, CustomNumFmt: &customNumFmt,
}) })
assert.NoError(t, err) assert.NoError(t, err)
result, err = f.formattedValue(1, "43528", false) result, err = f.formattedValue(&xlsxC{S: 1, V: "43528"}, false, CellTypeNumber)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "03/04/2019", result) assert.Equal(t, "03/04/2019", result)
@ -801,7 +982,7 @@ func TestFormattedValue(t *testing.T) {
f.Styles.CellXfs.Xf = append(f.Styles.CellXfs.Xf, xlsxXf{ f.Styles.CellXfs.Xf = append(f.Styles.CellXfs.Xf, xlsxXf{
NumFmtID: &numFmtID, NumFmtID: &numFmtID,
}) })
result, err = f.formattedValue(2, "43528", false) result, err = f.formattedValue(&xlsxC{S: 2, V: "43528"}, false, CellTypeNumber)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "43528", result) assert.Equal(t, "43528", result)
@ -809,7 +990,7 @@ func TestFormattedValue(t *testing.T) {
f.Styles.CellXfs.Xf = append(f.Styles.CellXfs.Xf, xlsxXf{ f.Styles.CellXfs.Xf = append(f.Styles.CellXfs.Xf, xlsxXf{
NumFmtID: nil, NumFmtID: nil,
}) })
result, err = f.formattedValue(3, "43528", false) result, err = f.formattedValue(&xlsxC{S: 3, V: "43528"}, false, CellTypeNumber)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "43528", result) assert.Equal(t, "43528", result)
@ -818,7 +999,16 @@ func TestFormattedValue(t *testing.T) {
f.Styles.CellXfs.Xf = append(f.Styles.CellXfs.Xf, xlsxXf{ f.Styles.CellXfs.Xf = append(f.Styles.CellXfs.Xf, xlsxXf{
NumFmtID: &numFmtID, NumFmtID: &numFmtID,
}) })
result, err = f.formattedValue(1, "43528", false) result, err = f.formattedValue(&xlsxC{S: 1, V: "43528"}, false, CellTypeNumber)
assert.NoError(t, err)
assert.Equal(t, "43528", result)
// Test format numeric value with shared string data type
f.Styles.NumFmts, numFmtID = nil, 11
f.Styles.CellXfs.Xf = append(f.Styles.CellXfs.Xf, xlsxXf{
NumFmtID: &numFmtID,
})
result, err = f.formattedValue(&xlsxC{S: 5, V: "43528"}, false, CellTypeSharedString)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "43528", result) assert.Equal(t, "43528", result)
@ -827,32 +1017,32 @@ func TestFormattedValue(t *testing.T) {
NumFmt: 1, NumFmt: 1,
}) })
assert.NoError(t, err) assert.NoError(t, err)
result, err = f.formattedValue(styleID, "310.56", false) result, err = f.formattedValue(&xlsxC{S: styleID, V: "310.56"}, false, CellTypeNumber)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "311", result) assert.Equal(t, "311", result)
for _, fn := range builtInNumFmtFunc { assert.Equal(t, "0_0", format("0_0", "", false, CellTypeNumber, nil))
assert.Equal(t, "0_0", fn("0_0", "", false))
}
// Test format value with unsupported charset workbook // Test format value with unsupported charset workbook
f.WorkBook = nil f.WorkBook = nil
f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset) f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset)
_, err = f.formattedValue(1, "43528", false) _, err = f.formattedValue(&xlsxC{S: 1, V: "43528"}, false, CellTypeNumber)
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
// Test format value with unsupported charset style sheet // Test format value with unsupported charset style sheet
f.Styles = nil f.Styles = nil
f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset) f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset)
_, err = f.formattedValue(1, "43528", false) _, err = f.formattedValue(&xlsxC{S: 1, V: "43528"}, false, CellTypeNumber)
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
assert.Equal(t, "text", format("text", "0", false, CellTypeNumber, nil))
} }
func TestFormattedValueNilXfs(t *testing.T) { func TestFormattedValueNilXfs(t *testing.T) {
// Set the CellXfs to nil and verify that the formattedValue function does not crash // Set the CellXfs to nil and verify that the formattedValue function does not crash
f := NewFile() f := NewFile()
f.Styles.CellXfs = nil f.Styles.CellXfs = nil
result, err := f.formattedValue(3, "43528", false) result, err := f.formattedValue(&xlsxC{S: 3, V: "43528"}, false, CellTypeNumber)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "43528", result) assert.Equal(t, "43528", result)
} }
@ -861,7 +1051,7 @@ func TestFormattedValueNilNumFmts(t *testing.T) {
// Set the NumFmts value to nil and verify that the formattedValue function does not crash // Set the NumFmts value to nil and verify that the formattedValue function does not crash
f := NewFile() f := NewFile()
f.Styles.NumFmts = nil f.Styles.NumFmts = nil
result, err := f.formattedValue(3, "43528", false) result, err := f.formattedValue(&xlsxC{S: 3, V: "43528"}, false, CellTypeNumber)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "43528", result) assert.Equal(t, "43528", result)
} }
@ -870,7 +1060,7 @@ func TestFormattedValueNilWorkbook(t *testing.T) {
// Set the Workbook value to nil and verify that the formattedValue function does not crash // Set the Workbook value to nil and verify that the formattedValue function does not crash
f := NewFile() f := NewFile()
f.WorkBook = nil f.WorkBook = nil
result, err := f.formattedValue(3, "43528", false) result, err := f.formattedValue(&xlsxC{S: 3, V: "43528"}, false, CellTypeNumber)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "43528", result) assert.Equal(t, "43528", result)
} }
@ -880,11 +1070,21 @@ func TestFormattedValueNilWorkbookPr(t *testing.T) {
// crash. // crash.
f := NewFile() f := NewFile()
f.WorkBook.WorkbookPr = nil f.WorkBook.WorkbookPr = nil
result, err := f.formattedValue(3, "43528", false) result, err := f.formattedValue(&xlsxC{S: 3, V: "43528"}, false, CellTypeNumber)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "43528", result) assert.Equal(t, "43528", result)
} }
func TestGetCustomNumFmtCode(t *testing.T) {
expected := "[$-ja-JP-x-gannen,80]ggge\"年\"m\"月\"d\"日\";@"
styleSheet := &xlsxStyleSheet{NumFmts: &xlsxNumFmts{NumFmt: []*xlsxNumFmt{
{NumFmtID: 164, FormatCode16: expected},
}}}
numFmtCode, ok := styleSheet.getCustomNumFmtCode(164)
assert.Equal(t, expected, numFmtCode)
assert.True(t, ok)
}
func TestSharedStringsError(t *testing.T) { func TestSharedStringsError(t *testing.T) {
f, err := OpenFile(filepath.Join("test", "Book1.xlsx"), Options{UnzipXMLSizeLimit: 128}) f, err := OpenFile(filepath.Join("test", "Book1.xlsx"), Options{UnzipXMLSizeLimit: 128})
assert.NoError(t, err) assert.NoError(t, err)

160
chart.go
View File

@ -1,4 +1,4 @@
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in // this source code is governed by a BSD-style license that can be found in
// the LICENSE file. // the LICENSE file.
// //
@ -7,7 +7,7 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming // Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of // API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.16 or later. // data. This library needs Go version 1.18 or later.
package excelize package excelize
@ -80,6 +80,30 @@ const (
Bubble3D Bubble3D
) )
// ChartLineType is the type of supported chart line types.
type ChartLineType byte
// This section defines the currently supported chart line types enumeration.
const (
ChartLineUnset ChartLineType = iota
ChartLineSolid
ChartLineNone
ChartLineAutomatic
)
// ChartTickLabelPositionType is the type of supported chart tick label position
// types.
type ChartTickLabelPositionType byte
// This section defines the supported chart tick label position types
// enumeration.
const (
ChartTickLabelNextToAxis ChartTickLabelPositionType = iota
ChartTickLabelHigh
ChartTickLabelLow
ChartTickLabelNone
)
// This section defines the default value of chart properties. // This section defines the default value of chart properties.
var ( var (
chartView3DRotX = map[ChartType]int{ chartView3DRotX = map[ChartType]int{
@ -474,7 +498,13 @@ var (
true: "r", true: "r",
false: "l", false: "l",
} }
valTickLblPos = map[ChartType]string{ tickLblPosVal = map[ChartTickLabelPositionType]string{
ChartTickLabelNextToAxis: "nextTo",
ChartTickLabelHigh: "high",
ChartTickLabelLow: "low",
ChartTickLabelNone: "none",
}
tickLblPosNone = map[ChartType]string{
Contour: "none", Contour: "none",
WireframeContour: "none", WireframeContour: "none",
} }
@ -499,26 +529,42 @@ func parseChartOptions(opts *Chart) (*Chart, error) {
opts.Format.Locked = boolPtr(false) opts.Format.Locked = boolPtr(false)
} }
if opts.Format.ScaleX == 0 { if opts.Format.ScaleX == 0 {
opts.Format.ScaleX = defaultPictureScale opts.Format.ScaleX = defaultDrawingScale
} }
if opts.Format.ScaleY == 0 { if opts.Format.ScaleY == 0 {
opts.Format.ScaleY = defaultPictureScale opts.Format.ScaleY = defaultDrawingScale
} }
if opts.Legend.Position == "" { if opts.Legend.Position == "" {
opts.Legend.Position = defaultChartLegendPosition opts.Legend.Position = defaultChartLegendPosition
} }
if opts.Title.Name == "" { opts.parseTitle()
opts.Title.Name = " "
}
if opts.VaryColors == nil { if opts.VaryColors == nil {
opts.VaryColors = boolPtr(true) opts.VaryColors = boolPtr(true)
} }
if opts.Border.Width == 0 {
opts.Border.Width = 0.75
}
if opts.ShowBlanksAs == "" { if opts.ShowBlanksAs == "" {
opts.ShowBlanksAs = defaultChartShowBlanksAs opts.ShowBlanksAs = defaultChartShowBlanksAs
} }
return opts, nil return opts, nil
} }
// parseTitle parse the title settings of the chart with default value.
func (opts *Chart) parseTitle() {
for i := range opts.Title {
if opts.Title[i].Font == nil {
opts.Title[i].Font = &Font{}
}
if opts.Title[i].Font.Color == "" {
opts.Title[i].Font.Color = "595959"
}
if opts.Title[i].Font.Size == 0 {
opts.Title[i].Font.Size = 14
}
}
}
// AddChart provides the method to add chart in a sheet by given chart format // AddChart provides the method to add chart in a sheet by given chart format
// set (such as offset, scale, aspect ratio setting and print settings) and // set (such as offset, scale, aspect ratio setting and print settings) and
// properties set. For example, create 3D clustered column chart with data // properties set. For example, create 3D clustered column chart with data
@ -569,8 +615,10 @@ func parseChartOptions(opts *Chart) (*Chart, error) {
// Values: "Sheet1!$B$4:$D$4", // Values: "Sheet1!$B$4:$D$4",
// }, // },
// }, // },
// Title: excelize.ChartTitle{ // Title: []excelize.RichTextRun{
// Name: "Fruit 3D Clustered Column Chart", // {
// Text: "Fruit 3D Clustered Column Chart",
// },
// }, // },
// Legend: excelize.ChartLegend{ // Legend: excelize.ChartLegend{
// ShowLegendKey: false, // ShowLegendKey: false,
@ -660,11 +708,11 @@ func parseChartOptions(opts *Chart) (*Chart, error) {
// //
// Name // Name
// Categories // Categories
// Sizes
// Values // Values
// Fill // Fill
// Line // Line
// Marker // Marker
// DataLabelPosition
// //
// Name: Set the name for the series. The name is displayed in the chart legend // Name: Set the name for the series. The name is displayed in the chart legend
// and in the formula bar. The 'Name' property is optional and if it isn't // and in the formula bar. The 'Name' property is optional and if it isn't
@ -675,13 +723,15 @@ func parseChartOptions(opts *Chart) (*Chart, error) {
// the same as the X axis. In most chart types the 'Categories' property is // the same as the X axis. In most chart types the 'Categories' property is
// optional and the chart will just assume a sequential series from 1..n. // optional and the chart will just assume a sequential series from 1..n.
// //
// Sizes: This sets the bubble size in a data series.
//
// Values: This is the most important property of a series and is the only // Values: This is the most important property of a series and is the only
// mandatory option for every chart object. This option links the chart with // mandatory option for every chart object. This option links the chart with
// the worksheet data that it displays. // the worksheet data that it displays.
// //
// Fill: This set the format for the data series fill. // Sizes: This sets the bubble size in a data series. The 'Sizes' property is
// optional and the default value was same with 'Values'.
//
// Fill: This set the format for the data series fill. The 'Fill' property is
// optional
// //
// Line: This sets the line format of the line chart. The 'Line' property is // Line: This sets the line format of the line chart. The 'Line' property is
// optional and if it isn't supplied it will default style. The options that // optional and if it isn't supplied it will default style. The options that
@ -705,6 +755,8 @@ func parseChartOptions(opts *Chart) (*Chart, error) {
// x // x
// auto // auto
// //
// DataLabelPosition: This sets the position of the chart series data label.
//
// Set properties of the chart legend. The options that can be set are: // Set properties of the chart legend. The options that can be set are:
// //
// Position // Position
@ -727,7 +779,7 @@ func parseChartOptions(opts *Chart) (*Chart, error) {
// //
// Title // Title
// //
// Name: Set the name (title) for the chart. The name is displayed above the // Title: Set the name (title) for the chart. The name is displayed above the
// chart. The name can also be a formula such as Sheet1!$A$1 or a list with a // chart. The name can also be a formula such as Sheet1!$A$1 or a list with a
// sheet name. The name property is optional. The default is to have no chart // sheet name. The name property is optional. The default is to have no chart
// title. // title.
@ -748,11 +800,11 @@ func parseChartOptions(opts *Chart) (*Chart, error) {
// Specifies that each data marker in the series has a different color by // Specifies that each data marker in the series has a different color by
// 'VaryColors'. The default value is true. // 'VaryColors'. The default value is true.
// //
// Set chart offset, scale, aspect ratio setting and print settings by format, // Set chart offset, scale, aspect ratio setting and print settings by 'Format',
// same as function 'AddPicture'. // same as function 'AddPicture'.
// //
// Set the position of the chart plot area by PlotArea. The properties that can // Set the position of the chart plot area by 'PlotArea'. The properties that
// be set are: // can be set are:
// //
// SecondPlotValues // SecondPlotValues
// ShowBubbleSize // ShowBubbleSize
@ -761,6 +813,7 @@ func parseChartOptions(opts *Chart) (*Chart, error) {
// ShowPercent // ShowPercent
// ShowSerName // ShowSerName
// ShowVal // ShowVal
// NumFmt
// //
// SecondPlotValues: Specifies the values in second plot for the 'pieOfPie' and // SecondPlotValues: Specifies the values in second plot for the 'pieOfPie' and
// 'barOfPie' chart. // 'barOfPie' chart.
@ -783,6 +836,10 @@ func parseChartOptions(opts *Chart) (*Chart, error) {
// ShowVal: Specifies that the value shall be shown in a data label. // ShowVal: Specifies that the value shall be shown in a data label.
// The 'ShowVal' property is optional. The default value is false. // The 'ShowVal' property is optional. The default value is false.
// //
// NumFmt: Specifies that if linked to source and set custom number format code
// for data labels. The 'NumFmt' property is optional. The default format code
// is 'General'.
//
// Set the primary horizontal and vertical axis options by 'XAxis' and 'YAxis'. // Set the primary horizontal and vertical axis options by 'XAxis' and 'YAxis'.
// The properties of 'XAxis' that can be set are: // The properties of 'XAxis' that can be set are:
// //
@ -793,7 +850,10 @@ func parseChartOptions(opts *Chart) (*Chart, error) {
// ReverseOrder // ReverseOrder
// Maximum // Maximum
// Minimum // Minimum
// Alignment
// Font // Font
// NumFmt
// Title
// //
// The properties of 'YAxis' that can be set are: // The properties of 'YAxis' that can be set are:
// //
@ -801,10 +861,15 @@ func parseChartOptions(opts *Chart) (*Chart, error) {
// MajorGridLines // MajorGridLines
// MinorGridLines // MinorGridLines
// MajorUnit // MajorUnit
// Secondary
// ReverseOrder // ReverseOrder
// Maximum // Maximum
// Minimum // Minimum
// Alignment
// Font // Font
// LogBase
// NumFmt
// Title
// //
// None: Disable axes. // None: Disable axes.
// //
@ -813,14 +878,18 @@ func parseChartOptions(opts *Chart) (*Chart, error) {
// MinorGridLines: Specifies minor grid lines. // MinorGridLines: Specifies minor grid lines.
// //
// MajorUnit: Specifies the distance between major ticks. Shall contain a // MajorUnit: Specifies the distance between major ticks. Shall contain a
// positive floating-point number. The MajorUnit property is optional. The // positive floating-point number. The 'MajorUnit' property is optional. The
// default value is auto. // default value is auto.
// //
// Secondary: Specifies the current series vertical axis as the secondary axis,
// this only works for the second and later chart in the combo chart. The
// default value is false.
//
// TickLabelSkip: Specifies how many tick labels to skip between label that is // TickLabelSkip: Specifies how many tick labels to skip between label that is
// drawn. The 'TickLabelSkip' property is optional. The default value is auto. // drawn. The 'TickLabelSkip' property is optional. The default value is auto.
// //
// ReverseOrder: Specifies that the categories or values on reverse order // ReverseOrder: Specifies that the categories or values on reverse order
// (orientation of the chart). The ReverseOrder property is optional. The // (orientation of the chart). The 'ReverseOrder' property is optional. The
// default value is false. // default value is false.
// //
// Maximum: Specifies that the fixed maximum, 0 is auto. The 'Maximum' property // Maximum: Specifies that the fixed maximum, 0 is auto. The 'Maximum' property
@ -829,6 +898,24 @@ func parseChartOptions(opts *Chart) (*Chart, error) {
// Minimum: Specifies that the fixed minimum, 0 is auto. The 'Minimum' property // Minimum: Specifies that the fixed minimum, 0 is auto. The 'Minimum' property
// is optional. The default value is auto. // is optional. The default value is auto.
// //
// Alignment: Specifies that the alignment of the horizontal and vertical axis.
// The properties of font that can be set are:
//
// TextRotation
// Vertical
//
// The value of 'TextRotation' that can be set from -90 to 90:
//
// The value of 'Vertical' that can be set are:
//
// horz
// vert
// vert270
// wordArtVert
// eaVert
// mongolianVert
// wordArtVertRtl
//
// Font: Specifies that the font of the horizontal and vertical axis. The // Font: Specifies that the font of the horizontal and vertical axis. The
// properties of font that can be set are: // properties of font that can be set are:
// //
@ -841,8 +928,26 @@ func parseChartOptions(opts *Chart) (*Chart, error) {
// Color // Color
// VertAlign // VertAlign
// //
// LogBase: Specifies logarithmic scale base number of the vertical axis.
//
// NumFmt: Specifies that if linked to source and set custom number format code
// for axis. The 'NumFmt' property is optional. The default format code is
// 'General'.
//
// Title: Specifies that the primary horizontal or vertical axis title and
// resize chart. The 'Title' property is optional.
//
// Set chart size by 'Dimension' property. The 'Dimension' property is optional. // Set chart size by 'Dimension' property. The 'Dimension' property is optional.
// The default width is 480, and height is 290. // The default width is 480, and height is 260.
//
// Set the bubble size in all data series for the bubble chart or 3D bubble
// chart by 'BubbleSizes' property. The 'BubbleSizes' property is optional. The
// default width is 100, and the value should be great than 0 and less or equal
// than 300.
//
// Set the doughnut hole size in all data series for the doughnut chart by
// 'HoleSize' property. The 'HoleSize' property is optional. The default width
// is 75, and the value should be great than 0 and less or equal than 90.
// //
// combo: Specifies the create a chart that combines two or more chart types in // combo: Specifies the create a chart that combines two or more chart types in
// a single chart. For example, create a clustered column - line chart with // a single chart. For example, create a clustered column - line chart with
@ -876,7 +981,7 @@ func parseChartOptions(opts *Chart) (*Chart, error) {
// } // }
// enable, disable := true, false // enable, disable := true, false
// if err := f.AddChart("Sheet1", "E1", &excelize.Chart{ // if err := f.AddChart("Sheet1", "E1", &excelize.Chart{
// Type: "col", // Type: excelize.Col,
// Series: []excelize.ChartSeries{ // Series: []excelize.ChartSeries{
// { // {
// Name: "Sheet1!$A$2", // Name: "Sheet1!$A$2",
@ -893,8 +998,10 @@ func parseChartOptions(opts *Chart) (*Chart, error) {
// LockAspectRatio: false, // LockAspectRatio: false,
// Locked: &disable, // Locked: &disable,
// }, // },
// Title: excelize.ChartTitle{ // Title: []excelize.RichTextRun{
// Name: "Clustered Column - Line Chart", // {
// Text: "Clustered Column - Line Chart",
// },
// }, // },
// Legend: excelize.ChartLegend{ // Legend: excelize.ChartLegend{
// Position: "left", // Position: "left",
@ -908,7 +1015,7 @@ func parseChartOptions(opts *Chart) (*Chart, error) {
// ShowVal: true, // ShowVal: true,
// }, // },
// }, &excelize.Chart{ // }, &excelize.Chart{
// Type: "line", // Type: excelize.Line,
// Series: []excelize.ChartSeries{ // Series: []excelize.ChartSeries{
// { // {
// Name: "Sheet1!$A$4", // Name: "Sheet1!$A$4",
@ -1078,7 +1185,8 @@ func (f *File) DeleteChart(sheet, cell string) error {
return err return err
} }
drawingXML := strings.ReplaceAll(f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID), "..", "xl") drawingXML := strings.ReplaceAll(f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID), "..", "xl")
return f.deleteDrawing(col, row, drawingXML, "Chart") _, err = f.deleteDrawing(col, row, drawingXML, "Chart")
return err
} }
// countCharts provides a function to get chart files count storage in the // countCharts provides a function to get chart files count storage in the

View File

@ -52,7 +52,7 @@ func TestChartSize(t *testing.T) {
{Name: "Sheet1!$A$3", Categories: "Sheet1!$B$1:$D$1", Values: "Sheet1!$B$3:$D$3"}, {Name: "Sheet1!$A$3", Categories: "Sheet1!$B$1:$D$1", Values: "Sheet1!$B$3:$D$3"},
{Name: "Sheet1!$A$4", Categories: "Sheet1!$B$1:$D$1", Values: "Sheet1!$B$4:$D$4"}, {Name: "Sheet1!$A$4", Categories: "Sheet1!$B$1:$D$1", Values: "Sheet1!$B$4:$D$4"},
}, },
Title: ChartTitle{Name: "3D Clustered Column Chart"}, Title: []RichTextRun{{Text: "3D Clustered Column Chart"}},
})) }))
var buffer bytes.Buffer var buffer bytes.Buffer
@ -70,7 +70,7 @@ func TestChartSize(t *testing.T) {
var ( var (
workdir decodeWsDr workdir decodeWsDr
anchor decodeTwoCellAnchor anchor decodeCellAnchor
) )
content, ok := newFile.Pkg.Load("xl/drawings/drawing1.xml") content, ok := newFile.Pkg.Load("xl/drawings/drawing1.xml")
@ -81,8 +81,8 @@ func TestChartSize(t *testing.T) {
t.FailNow() t.FailNow()
} }
err = xml.Unmarshal([]byte("<decodeTwoCellAnchor>"+ err = xml.Unmarshal([]byte("<decodeCellAnchor>"+
workdir.TwoCellAnchor[0].Content+"</decodeTwoCellAnchor>"), &anchor) workdir.TwoCellAnchor[0].Content+"</decodeCellAnchor>"), &anchor)
if !assert.NoError(t, err) { if !assert.NoError(t, err) {
t.FailNow() t.FailNow()
} }
@ -94,7 +94,7 @@ func TestChartSize(t *testing.T) {
} }
if !assert.Equal(t, 14, anchor.To.Col, "Expected 'to' column 14") || if !assert.Equal(t, 14, anchor.To.Col, "Expected 'to' column 14") ||
!assert.Equal(t, 27, anchor.To.Row, "Expected 'to' row 27") { !assert.Equal(t, 29, anchor.To.Row, "Expected 'to' row 29") {
t.FailNow() t.FailNow()
} }
@ -120,7 +120,15 @@ func TestDeleteDrawing(t *testing.T) {
f := NewFile() f := NewFile()
path := "xl/drawings/drawing1.xml" path := "xl/drawings/drawing1.xml"
f.Pkg.Store(path, MacintoshCyrillicCharset) f.Pkg.Store(path, MacintoshCyrillicCharset)
assert.EqualError(t, f.deleteDrawing(0, 0, path, "Chart"), "XML syntax error on line 1: invalid UTF-8") _, err := f.deleteDrawing(0, 0, path, "Chart")
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
f, err = OpenFile(filepath.Join("test", "Book1.xlsx"))
assert.NoError(t, err)
f.Drawings.Store(path, &xlsxWsDr{TwoCellAnchor: []*xdrCellAnchor{{
GraphicFrame: string(MacintoshCyrillicCharset),
}}})
_, err = f.deleteDrawing(0, 0, path, "Chart")
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
} }
func TestAddChart(t *testing.T) { func TestAddChart(t *testing.T) {
@ -150,7 +158,12 @@ func TestAddChart(t *testing.T) {
{Name: "Sheet1!$A$34", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$34:$D$34"}, {Name: "Sheet1!$A$34", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$34:$D$34"},
{Name: "Sheet1!$A$35", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$35:$D$35"}, {Name: "Sheet1!$A$35", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$35:$D$35"},
{Name: "Sheet1!$A$36", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$36:$D$36"}, {Name: "Sheet1!$A$36", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$36:$D$36"},
{Name: "Sheet1!$A$37", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$37:$D$37"}, {
Name: "Sheet1!$A$37", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$37:$D$37",
Marker: ChartMarker{
Fill: Fill{Type: "pattern", Color: []string{"FFFF00"}, Pattern: 1},
},
},
} }
series2 := []ChartSeries{ series2 := []ChartSeries{
{ {
@ -168,18 +181,18 @@ func TestAddChart(t *testing.T) {
} }
series3 := []ChartSeries{{Name: "Sheet1!$A$30", Categories: "Sheet1!$A$30:$D$37", Values: "Sheet1!$B$30:$B$37"}} series3 := []ChartSeries{{Name: "Sheet1!$A$30", Categories: "Sheet1!$A$30:$D$37", Values: "Sheet1!$B$30:$B$37"}}
series4 := []ChartSeries{ series4 := []ChartSeries{
{Name: "Sheet1!$A$30", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$30:$D$30", Sizes: "Sheet1!$B$30:$D$30"}, {Name: "Sheet1!$A$30", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$30:$D$30", Sizes: "Sheet1!$B$30:$D$30", DataLabelPosition: ChartDataLabelsPositionAbove},
{Name: "Sheet1!$A$31", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$31:$D$31", Sizes: "Sheet1!$B$31:$D$31"}, {Name: "Sheet1!$A$31", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$31:$D$31", Sizes: "Sheet1!$B$31:$D$31", DataLabelPosition: ChartDataLabelsPositionLeft},
{Name: "Sheet1!$A$32", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$32:$D$32", Sizes: "Sheet1!$B$32:$D$32"}, {Name: "Sheet1!$A$32", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$32:$D$32", Sizes: "Sheet1!$B$32:$D$32", DataLabelPosition: ChartDataLabelsPositionBestFit},
{Name: "Sheet1!$A$33", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$33:$D$33", Sizes: "Sheet1!$B$33:$D$33"}, {Name: "Sheet1!$A$33", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$33:$D$33", Sizes: "Sheet1!$B$33:$D$33", DataLabelPosition: ChartDataLabelsPositionCenter},
{Name: "Sheet1!$A$34", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$34:$D$34", Sizes: "Sheet1!$B$34:$D$34"}, {Name: "Sheet1!$A$34", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$34:$D$34", Sizes: "Sheet1!$B$34:$D$34", DataLabelPosition: ChartDataLabelsPositionInsideBase},
{Name: "Sheet1!$A$35", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$35:$D$35", Sizes: "Sheet1!$B$35:$D$35"}, {Name: "Sheet1!$A$35", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$35:$D$35", Sizes: "Sheet1!$B$35:$D$35", DataLabelPosition: ChartDataLabelsPositionInsideEnd},
{Name: "Sheet1!$A$36", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$36:$D$36", Sizes: "Sheet1!$B$36:$D$36"}, {Name: "Sheet1!$A$36", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$36:$D$36", Sizes: "Sheet1!$B$36:$D$36", DataLabelPosition: ChartDataLabelsPositionOutsideEnd},
{Name: "Sheet1!$A$37", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$37:$D$37", Sizes: "Sheet1!$B$37:$D$37"}, {Name: "Sheet1!$A$37", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$37:$D$37", Sizes: "Sheet1!$B$37:$D$37", DataLabelPosition: ChartDataLabelsPositionRight},
} }
format := GraphicOptions{ format := GraphicOptions{
ScaleX: defaultPictureScale, ScaleX: defaultDrawingScale,
ScaleY: defaultPictureScale, ScaleY: defaultDrawingScale,
OffsetX: 15, OffsetX: 15,
OffsetY: 10, OffsetY: 10,
PrintObject: boolPtr(true), PrintObject: boolPtr(true),
@ -195,74 +208,75 @@ func TestAddChart(t *testing.T) {
ShowPercent: true, ShowPercent: true,
ShowSerName: true, ShowSerName: true,
ShowVal: true, ShowVal: true,
Fill: Fill{Type: "pattern", Pattern: 1},
} }
for _, c := range []struct { for _, c := range []struct {
sheetName, cell string sheetName, cell string
opts *Chart opts *Chart
}{ }{
{sheetName: "Sheet1", cell: "P1", opts: &Chart{Type: Col, Series: series, Format: format, Legend: ChartLegend{Position: "none", ShowLegendKey: true}, Title: ChartTitle{Name: "2D Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{Font: Font{Bold: true, Italic: true, Underline: "dbl", Color: "000000"}}, YAxis: ChartAxis{Font: Font{Bold: false, Italic: false, Underline: "sng", Color: "777777"}}}}, {sheetName: "Sheet1", cell: "P1", opts: &Chart{Type: Col, Series: series, Format: format, Legend: ChartLegend{Position: "none", ShowLegendKey: true}, Title: []RichTextRun{{Text: "2D Column Chart"}}, PlotArea: plotArea, Border: ChartLine{Type: ChartLineNone}, ShowBlanksAs: "zero", XAxis: ChartAxis{Font: Font{Bold: true, Italic: true, Underline: "dbl", Family: "Times New Roman", Size: 15, Strike: true, Color: "000000"}, Title: []RichTextRun{{Text: "Primary Horizontal Axis Title"}}}, YAxis: ChartAxis{Font: Font{Bold: false, Italic: false, Underline: "sng", Color: "777777"}, Title: []RichTextRun{{Text: "Primary Vertical Axis Title", Font: &Font{Color: "777777", Bold: true, Italic: true, Size: 12}}}}}},
{sheetName: "Sheet1", cell: "X1", opts: &Chart{Type: ColStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Stacked Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet1", cell: "X1", opts: &Chart{Type: ColStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "2D Stacked Column Chart"}}, PlotArea: plotArea, Fill: Fill{Type: "pattern", Pattern: 1}, Border: ChartLine{Type: ChartLineAutomatic}, ShowBlanksAs: "zero"}},
{sheetName: "Sheet1", cell: "P16", opts: &Chart{Type: ColPercentStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "100% Stacked Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet1", cell: "P16", opts: &Chart{Type: ColPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "100% Stacked Column Chart"}}, PlotArea: plotArea, Fill: Fill{Type: "pattern", Color: []string{"EEEEEE"}, Pattern: 1}, Border: ChartLine{Type: ChartLineSolid, Width: 2}, ShowBlanksAs: "zero", XAxis: ChartAxis{Alignment: Alignment{Vertical: "wordArtVertRtl", TextRotation: 0}}}},
{sheetName: "Sheet1", cell: "X16", opts: &Chart{Type: Col3DClustered, Series: series, Format: format, Legend: ChartLegend{Position: "bottom", ShowLegendKey: false}, Title: ChartTitle{Name: "3D Clustered Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet1", cell: "X16", opts: &Chart{Type: Col3DClustered, Series: series, Format: format, Legend: ChartLegend{Position: "bottom", ShowLegendKey: false}, Title: []RichTextRun{{Text: "3D Clustered Column Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
{sheetName: "Sheet1", cell: "P30", opts: &Chart{Type: Col3DStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Stacked Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet1", cell: "P30", opts: &Chart{Type: Col3DStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Stacked Column Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{Alignment: Alignment{Vertical: "vert", TextRotation: 0}}}},
{sheetName: "Sheet1", cell: "X30", opts: &Chart{Type: Col3DPercentStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D 100% Stacked Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet1", cell: "X30", opts: &Chart{Type: Col3DPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D 100% Stacked Column Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
{sheetName: "Sheet1", cell: "X45", opts: &Chart{Type: Radar, Series: series, Format: format, Legend: ChartLegend{Position: "top_right", ShowLegendKey: false}, Title: ChartTitle{Name: "Radar Chart"}, PlotArea: plotArea, ShowBlanksAs: "span"}}, {sheetName: "Sheet1", cell: "X45", opts: &Chart{Type: Radar, Series: series, Format: format, Legend: ChartLegend{Position: "top_right", ShowLegendKey: false}, Title: []RichTextRun{{Text: "Radar Chart"}}, PlotArea: plotArea, ShowBlanksAs: "span"}},
{sheetName: "Sheet1", cell: "AF1", opts: &Chart{Type: Col3DConeStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Cone Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet1", cell: "AF1", opts: &Chart{Type: Col3DConeStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Column Cone Stacked Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
{sheetName: "Sheet1", cell: "AF16", opts: &Chart{Type: Col3DConeClustered, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Cone Clustered Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet1", cell: "AF16", opts: &Chart{Type: Col3DConeClustered, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Column Cone Clustered Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
{sheetName: "Sheet1", cell: "AF30", opts: &Chart{Type: Col3DConePercentStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Cone Percent Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet1", cell: "AF30", opts: &Chart{Type: Col3DConePercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Column Cone Percent Stacked Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
{sheetName: "Sheet1", cell: "AF45", opts: &Chart{Type: Col3DCone, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Cone Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet1", cell: "AF45", opts: &Chart{Type: Col3DCone, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Column Cone Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
{sheetName: "Sheet1", cell: "AN1", opts: &Chart{Type: Col3DPyramidStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Pyramid Percent Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet1", cell: "AN1", opts: &Chart{Type: Col3DPyramidStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Column Pyramid Percent Stacked Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
{sheetName: "Sheet1", cell: "AN16", opts: &Chart{Type: Col3DPyramidClustered, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Pyramid Clustered Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet1", cell: "AN16", opts: &Chart{Type: Col3DPyramidClustered, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Column Pyramid Clustered Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
{sheetName: "Sheet1", cell: "AN30", opts: &Chart{Type: Col3DPyramidPercentStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Pyramid Percent Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet1", cell: "AN30", opts: &Chart{Type: Col3DPyramidPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Column Pyramid Percent Stacked Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
{sheetName: "Sheet1", cell: "AN45", opts: &Chart{Type: Col3DPyramid, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Pyramid Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet1", cell: "AN45", opts: &Chart{Type: Col3DPyramid, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Column Pyramid Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
{sheetName: "Sheet1", cell: "AV1", opts: &Chart{Type: Col3DCylinderStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Cylinder Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet1", cell: "AV1", opts: &Chart{Type: Col3DCylinderStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Column Cylinder Stacked Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
{sheetName: "Sheet1", cell: "AV16", opts: &Chart{Type: Col3DCylinderClustered, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Cylinder Clustered Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet1", cell: "AV16", opts: &Chart{Type: Col3DCylinderClustered, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Column Cylinder Clustered Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
{sheetName: "Sheet1", cell: "AV30", opts: &Chart{Type: Col3DCylinderPercentStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Cylinder Percent Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet1", cell: "AV30", opts: &Chart{Type: Col3DCylinderPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Column Cylinder Percent Stacked Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
{sheetName: "Sheet1", cell: "AV45", opts: &Chart{Type: Col3DCylinder, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Cylinder Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet1", cell: "AV45", opts: &Chart{Type: Col3DCylinder, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Column Cylinder Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
{sheetName: "Sheet1", cell: "P45", opts: &Chart{Type: Col3D, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet1", cell: "P45", opts: &Chart{Type: Col3D, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Column Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{Alignment: Alignment{Vertical: "vert270", TextRotation: 0}}}},
{sheetName: "Sheet2", cell: "P1", opts: &Chart{Type: Line3D, Series: series2, Format: format, Legend: ChartLegend{Position: "top", ShowLegendKey: false}, Title: ChartTitle{Name: "3D Line Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, TickLabelSkip: 1, NumFmt: ChartNumFmt{CustomNumFmt: "General"}}, YAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, MajorUnit: 1, NumFmt: ChartNumFmt{CustomNumFmt: "General"}}}}, {sheetName: "Sheet2", cell: "P1", opts: &Chart{Type: Line3D, Series: series2, Format: format, Legend: ChartLegend{Position: "top", ShowLegendKey: false}, Title: []RichTextRun{{Text: "3D Line Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, TickLabelSkip: 1, NumFmt: ChartNumFmt{CustomNumFmt: "General"}}, YAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, MajorUnit: 1, NumFmt: ChartNumFmt{CustomNumFmt: "General"}}}},
{sheetName: "Sheet2", cell: "X1", opts: &Chart{Type: Scatter, Series: series, Format: format, Legend: ChartLegend{Position: "bottom", ShowLegendKey: false}, Title: ChartTitle{Name: "Scatter Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet2", cell: "X1", opts: &Chart{Type: Scatter, Series: series, Format: format, Legend: ChartLegend{Position: "bottom", ShowLegendKey: false}, Title: []RichTextRun{{Text: "Scatter Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
{sheetName: "Sheet2", cell: "P16", opts: &Chart{Type: Doughnut, Series: series3, Format: format, Legend: ChartLegend{Position: "right", ShowLegendKey: false}, Title: ChartTitle{Name: "Doughnut Chart"}, PlotArea: ChartPlotArea{ShowBubbleSize: false, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: false, ShowVal: false}, ShowBlanksAs: "zero", HoleSize: 30}}, {sheetName: "Sheet2", cell: "P16", opts: &Chart{Type: Doughnut, Series: series3, Format: format, Legend: ChartLegend{Position: "right", ShowLegendKey: false}, Title: []RichTextRun{{Text: "Doughnut Chart"}}, PlotArea: ChartPlotArea{ShowBubbleSize: false, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: false, ShowVal: false}, ShowBlanksAs: "zero", HoleSize: 30}},
{sheetName: "Sheet2", cell: "X16", opts: &Chart{Type: Line, Series: series2, Format: format, Legend: ChartLegend{Position: "top", ShowLegendKey: false}, Title: ChartTitle{Name: "Line Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, TickLabelSkip: 1}, YAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, MajorUnit: 1}}}, {sheetName: "Sheet2", cell: "X16", opts: &Chart{Type: Line, Series: series2, Format: format, Legend: ChartLegend{Position: "top", ShowLegendKey: false}, Title: []RichTextRun{{Text: "Line Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, TickLabelSkip: 1, TickLabelPosition: ChartTickLabelLow}, YAxis: ChartAxis{MajorGridLines: true, MinorGridLines: true, MajorUnit: 1}}},
{sheetName: "Sheet2", cell: "P32", opts: &Chart{Type: Pie3D, Series: series3, Format: format, Legend: ChartLegend{Position: "bottom", ShowLegendKey: false}, Title: ChartTitle{Name: "3D Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet2", cell: "P32", opts: &Chart{Type: Pie3D, Series: series3, Format: format, Legend: ChartLegend{Position: "bottom", ShowLegendKey: false}, Title: []RichTextRun{{Text: "3D Column Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
{sheetName: "Sheet2", cell: "X32", opts: &Chart{Type: Pie, Series: series3, Format: format, Legend: ChartLegend{Position: "bottom", ShowLegendKey: false}, Title: ChartTitle{Name: "Pie Chart"}, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: false, ShowVal: false, NumFmt: ChartNumFmt{CustomNumFmt: "0.00%;0;;"}}, ShowBlanksAs: "gap"}}, {sheetName: "Sheet2", cell: "X32", opts: &Chart{Type: Pie, Series: series3, Format: format, Legend: ChartLegend{Position: "bottom", ShowLegendKey: false}, Title: []RichTextRun{{Text: "Pie Chart"}}, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: false, ShowVal: false, NumFmt: ChartNumFmt{CustomNumFmt: "0.00%;0;;"}}, ShowBlanksAs: "gap"}},
// bar series chart // bar series chart
{sheetName: "Sheet2", cell: "P48", opts: &Chart{Type: Bar, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Clustered Bar Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet2", cell: "P48", opts: &Chart{Type: Bar, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "2D Clustered Bar Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
{sheetName: "Sheet2", cell: "X48", opts: &Chart{Type: BarStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Stacked Bar Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet2", cell: "X48", opts: &Chart{Type: BarStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "2D Stacked Bar Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
{sheetName: "Sheet2", cell: "P64", opts: &Chart{Type: BarPercentStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Stacked 100% Bar Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet2", cell: "P64", opts: &Chart{Type: BarPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "2D Stacked 100% Bar Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
{sheetName: "Sheet2", cell: "X64", opts: &Chart{Type: Bar3DClustered, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Clustered Bar Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet2", cell: "X64", opts: &Chart{Type: Bar3DClustered, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Clustered Bar Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
{sheetName: "Sheet2", cell: "P80", opts: &Chart{Type: Bar3DStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Stacked Bar Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", YAxis: ChartAxis{Maximum: &maximum, Minimum: &minimum}}}, {sheetName: "Sheet2", cell: "P80", opts: &Chart{Type: Bar3DStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Stacked Bar Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", YAxis: ChartAxis{Maximum: &maximum, Minimum: &minimum}}},
{sheetName: "Sheet2", cell: "X80", opts: &Chart{Type: Bar3DPercentStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D 100% Stacked Bar Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{ReverseOrder: true, Minimum: &zero}, YAxis: ChartAxis{ReverseOrder: true, Minimum: &zero}}}, {sheetName: "Sheet2", cell: "X80", opts: &Chart{Type: Bar3DPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D 100% Stacked Bar Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{ReverseOrder: true, Secondary: true, Minimum: &zero}, YAxis: ChartAxis{ReverseOrder: true, Minimum: &zero}}},
// area series chart // area series chart
{sheetName: "Sheet2", cell: "AF1", opts: &Chart{Type: Area, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Area Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet2", cell: "AF1", opts: &Chart{Type: Area, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "2D Area Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
{sheetName: "Sheet2", cell: "AN1", opts: &Chart{Type: AreaStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Stacked Area Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet2", cell: "AN1", opts: &Chart{Type: AreaStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "2D Stacked Area Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
{sheetName: "Sheet2", cell: "AF16", opts: &Chart{Type: AreaPercentStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D 100% Stacked Area Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet2", cell: "AF16", opts: &Chart{Type: AreaPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "2D 100% Stacked Area Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
{sheetName: "Sheet2", cell: "AN16", opts: &Chart{Type: Area3D, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Area Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet2", cell: "AN16", opts: &Chart{Type: Area3D, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Area Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
{sheetName: "Sheet2", cell: "AF32", opts: &Chart{Type: Area3DStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Stacked Area Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet2", cell: "AF32", opts: &Chart{Type: Area3DStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Stacked Area Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
{sheetName: "Sheet2", cell: "AN32", opts: &Chart{Type: Area3DPercentStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D 100% Stacked Area Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet2", cell: "AN32", opts: &Chart{Type: Area3DPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D 100% Stacked Area Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
// cylinder series chart // cylinder series chart
{sheetName: "Sheet2", cell: "AF48", opts: &Chart{Type: Bar3DCylinderStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Bar Cylinder Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet2", cell: "AF48", opts: &Chart{Type: Bar3DCylinderStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Bar Cylinder Stacked Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
{sheetName: "Sheet2", cell: "AF64", opts: &Chart{Type: Bar3DCylinderClustered, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Bar Cylinder Clustered Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet2", cell: "AF64", opts: &Chart{Type: Bar3DCylinderClustered, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Bar Cylinder Clustered Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
{sheetName: "Sheet2", cell: "AF80", opts: &Chart{Type: Bar3DCylinderPercentStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Bar Cylinder Percent Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet2", cell: "AF80", opts: &Chart{Type: Bar3DCylinderPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Bar Cylinder Percent Stacked Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
// cone series chart // cone series chart
{sheetName: "Sheet2", cell: "AN48", opts: &Chart{Type: Bar3DConeStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Bar Cone Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet2", cell: "AN48", opts: &Chart{Type: Bar3DConeStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Bar Cone Stacked Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
{sheetName: "Sheet2", cell: "AN64", opts: &Chart{Type: Bar3DConeClustered, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Bar Cone Clustered Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet2", cell: "AN64", opts: &Chart{Type: Bar3DConeClustered, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Bar Cone Clustered Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
{sheetName: "Sheet2", cell: "AN80", opts: &Chart{Type: Bar3DConePercentStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Bar Cone Percent Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet2", cell: "AN80", opts: &Chart{Type: Bar3DConePercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Bar Cone Percent Stacked Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
{sheetName: "Sheet2", cell: "AV48", opts: &Chart{Type: Bar3DPyramidStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Bar Pyramid Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet2", cell: "AV48", opts: &Chart{Type: Bar3DPyramidStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Bar Pyramid Stacked Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
{sheetName: "Sheet2", cell: "AV64", opts: &Chart{Type: Bar3DPyramidClustered, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Bar Pyramid Clustered Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet2", cell: "AV64", opts: &Chart{Type: Bar3DPyramidClustered, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Bar Pyramid Clustered Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
{sheetName: "Sheet2", cell: "AV80", opts: &Chart{Type: Bar3DPyramidPercentStacked, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Bar Pyramid Percent Stacked Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet2", cell: "AV80", opts: &Chart{Type: Bar3DPyramidPercentStacked, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Bar Pyramid Percent Stacked Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
// surface series chart // surface series chart
{sheetName: "Sheet2", cell: "AV1", opts: &Chart{Type: Surface3D, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Surface Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", YAxis: ChartAxis{MajorGridLines: true}}}, {sheetName: "Sheet2", cell: "AV1", opts: &Chart{Type: Surface3D, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Surface Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", YAxis: ChartAxis{MajorGridLines: true}}},
{sheetName: "Sheet2", cell: "AV16", opts: &Chart{Type: WireframeSurface3D, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Wireframe Surface Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", YAxis: ChartAxis{MajorGridLines: true}}}, {sheetName: "Sheet2", cell: "AV16", opts: &Chart{Type: WireframeSurface3D, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "3D Wireframe Surface Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", YAxis: ChartAxis{MajorGridLines: true}}},
{sheetName: "Sheet2", cell: "AV32", opts: &Chart{Type: Contour, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "Contour Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet2", cell: "AV32", opts: &Chart{Type: Contour, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "Contour Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
{sheetName: "Sheet2", cell: "BD1", opts: &Chart{Type: WireframeContour, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "Wireframe Contour Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet2", cell: "BD1", opts: &Chart{Type: WireframeContour, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "Wireframe Contour Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
// bubble chart // bubble chart
{sheetName: "Sheet2", cell: "BD16", opts: &Chart{Type: Bubble, Series: series4, Format: format, Legend: legend, Title: ChartTitle{Name: "Bubble Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}}, {sheetName: "Sheet2", cell: "BD16", opts: &Chart{Type: Bubble, Series: series4, Format: format, Legend: legend, Title: []RichTextRun{{Text: "Bubble Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", BubbleSize: 75}},
{sheetName: "Sheet2", cell: "BD32", opts: &Chart{Type: Bubble3D, Series: series4, Format: format, Legend: legend, Title: ChartTitle{Name: "Bubble 3D Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true}, YAxis: ChartAxis{MajorGridLines: true}}}, {sheetName: "Sheet2", cell: "BD32", opts: &Chart{Type: Bubble3D, Series: series4, Format: format, Legend: legend, Title: []RichTextRun{{Text: "Bubble 3D Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true}, YAxis: ChartAxis{MajorGridLines: true}}},
// pie of pie chart // pie of pie chart
{sheetName: "Sheet2", cell: "BD48", opts: &Chart{Type: PieOfPie, Series: series3, Format: format, Legend: legend, Title: ChartTitle{Name: "Pie of Pie Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true}, YAxis: ChartAxis{MajorGridLines: true}}}, {sheetName: "Sheet2", cell: "BD48", opts: &Chart{Type: PieOfPie, Series: series3, Format: format, Legend: legend, Title: []RichTextRun{{Text: "Pie of Pie Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true}, YAxis: ChartAxis{MajorGridLines: true}}},
// bar of pie chart // bar of pie chart
{sheetName: "Sheet2", cell: "BD64", opts: &Chart{Type: BarOfPie, Series: series3, Format: format, Legend: legend, Title: ChartTitle{Name: "Bar of Pie Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true}, YAxis: ChartAxis{MajorGridLines: true}}}, {sheetName: "Sheet2", cell: "BD64", opts: &Chart{Type: BarOfPie, Series: series3, Format: format, Legend: legend, Title: []RichTextRun{{Text: "Bar of Pie Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true}, YAxis: ChartAxis{MajorGridLines: true}}},
} { } {
assert.NoError(t, f.AddChart(c.sheetName, c.cell, c.opts)) assert.NoError(t, f.AddChart(c.sheetName, c.cell, c.opts))
} }
@ -274,32 +288,32 @@ func TestAddChart(t *testing.T) {
{"I1", Doughnut, "Clustered Column - Doughnut Chart"}, {"I1", Doughnut, "Clustered Column - Doughnut Chart"},
} }
for _, props := range clusteredColumnCombo { for _, props := range clusteredColumnCombo {
assert.NoError(t, f.AddChart("Combo Charts", props[0].(string), &Chart{Type: Col, Series: series[:4], Format: format, Legend: legend, Title: ChartTitle{Name: props[2].(string)}, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: true, ShowVal: true}}, &Chart{Type: props[1].(ChartType), Series: series[4:], Format: format, Legend: legend, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: true, ShowVal: true}})) assert.NoError(t, f.AddChart("Combo Charts", props[0].(string), &Chart{Type: Col, Series: series[:4], Format: format, Legend: legend, Title: []RichTextRun{{Text: props[2].(string)}}, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: true, ShowVal: true}}, &Chart{Type: props[1].(ChartType), Series: series[4:], Format: format, Legend: legend, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: true, ShowVal: true}, YAxis: ChartAxis{Secondary: true}}))
} }
stackedAreaCombo := map[string][]interface{}{ stackedAreaCombo := map[string][]interface{}{
"A16": {Line, "Stacked Area - Line Chart"}, "A16": {Line, "Stacked Area - Line Chart"},
"I16": {Doughnut, "Stacked Area - Doughnut Chart"}, "I16": {Doughnut, "Stacked Area - Doughnut Chart"},
} }
for axis, props := range stackedAreaCombo { for axis, props := range stackedAreaCombo {
assert.NoError(t, f.AddChart("Combo Charts", axis, &Chart{Type: AreaStacked, Series: series[:4], Format: format, Legend: legend, Title: ChartTitle{Name: props[1].(string)}, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: true, ShowVal: true}}, &Chart{Type: props[0].(ChartType), Series: series[4:], Format: format, Legend: legend, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: true, ShowVal: true}})) assert.NoError(t, f.AddChart("Combo Charts", axis, &Chart{Type: AreaStacked, Series: series[:4], Format: format, Legend: legend, Title: []RichTextRun{{Text: props[1].(string)}}, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: true, ShowVal: true}}, &Chart{Type: props[0].(ChartType), Series: series[4:], Format: format, Legend: legend, PlotArea: ChartPlotArea{ShowBubbleSize: true, ShowCatName: false, ShowLeaderLines: false, ShowPercent: true, ShowSerName: true, ShowVal: true}}))
} }
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddChart.xlsx"))) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddChart.xlsx")))
// Test with invalid sheet name // Test with invalid sheet name
assert.EqualError(t, f.AddChart("Sheet:1", "A1", &Chart{Type: Col, Series: series[:1]}), ErrSheetNameInvalid.Error()) assert.EqualError(t, f.AddChart("Sheet:1", "A1", &Chart{Type: Col, Series: series[:1]}), ErrSheetNameInvalid.Error())
// Test with illegal cell reference // Test with illegal cell reference
assert.EqualError(t, f.AddChart("Sheet2", "A", &Chart{Type: Col, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.EqualError(t, f.AddChart("Sheet2", "A", &Chart{Type: Col, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "2D Column Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
// Test with unsupported chart type // Test with unsupported chart type
assert.EqualError(t, f.AddChart("Sheet2", "BD32", &Chart{Type: 0x37, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "Bubble 3D Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}), newUnsupportedChartType(0x37).Error()) assert.EqualError(t, f.AddChart("Sheet2", "BD32", &Chart{Type: 0x37, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "Bubble 3D Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}), newUnsupportedChartType(0x37).Error())
// Test add combo chart with invalid format set // Test add combo chart with invalid format set
assert.EqualError(t, f.AddChart("Sheet2", "BD32", &Chart{Type: Col, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}, nil), ErrParameterInvalid.Error()) assert.EqualError(t, f.AddChart("Sheet2", "BD32", &Chart{Type: Col, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "2D Column Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}, nil), ErrParameterInvalid.Error())
// Test add combo chart with unsupported chart type // Test add combo chart with unsupported chart type
assert.EqualError(t, f.AddChart("Sheet2", "BD64", &Chart{Type: BarOfPie, Series: []ChartSeries{{Name: "Sheet1!$A$30", Categories: "Sheet1!$A$30:$D$37", Values: "Sheet1!$B$30:$B$37"}}, Format: format, Legend: legend, Title: ChartTitle{Name: "Bar of Pie Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true}, YAxis: ChartAxis{MajorGridLines: true}}, &Chart{Type: 0x37, Series: []ChartSeries{{Name: "Sheet1!$A$30", Categories: "Sheet1!$A$30:$D$37", Values: "Sheet1!$B$30:$B$37"}}, Format: format, Legend: legend, Title: ChartTitle{Name: "Bar of Pie Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true}, YAxis: ChartAxis{MajorGridLines: true}}), newUnsupportedChartType(0x37).Error()) assert.EqualError(t, f.AddChart("Sheet2", "BD64", &Chart{Type: BarOfPie, Series: []ChartSeries{{Name: "Sheet1!$A$30", Categories: "Sheet1!$A$30:$D$37", Values: "Sheet1!$B$30:$B$37"}}, Format: format, Legend: legend, Title: []RichTextRun{{Text: "Bar of Pie Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true}, YAxis: ChartAxis{MajorGridLines: true}}, &Chart{Type: 0x37, Series: []ChartSeries{{Name: "Sheet1!$A$30", Categories: "Sheet1!$A$30:$D$37", Values: "Sheet1!$B$30:$B$37"}}, Format: format, Legend: legend, Title: []RichTextRun{{Text: "Bar of Pie Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true}, YAxis: ChartAxis{MajorGridLines: true}}), newUnsupportedChartType(0x37).Error())
assert.NoError(t, f.Close()) assert.NoError(t, f.Close())
// Test add chart with unsupported charset content types. // Test add chart with unsupported charset content types.
f.ContentTypes = nil f.ContentTypes = nil
f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
assert.EqualError(t, f.AddChart("Sheet1", "P1", &Chart{Type: Col, Series: []ChartSeries{{Name: "Sheet1!$A$30", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$30:$D$30"}}, Title: ChartTitle{Name: "2D Column Chart"}}), "XML syntax error on line 1: invalid UTF-8") assert.EqualError(t, f.AddChart("Sheet1", "P1", &Chart{Type: Col, Series: []ChartSeries{{Name: "Sheet1!$A$30", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$30:$D$30"}}, Title: []RichTextRun{{Text: "2D Column Chart"}}}), "XML syntax error on line 1: invalid UTF-8")
} }
func TestAddChartSheet(t *testing.T) { func TestAddChartSheet(t *testing.T) {
@ -317,7 +331,7 @@ func TestAddChartSheet(t *testing.T) {
{Name: "Sheet1!$A$3", Categories: "Sheet1!$B$1:$D$1", Values: "Sheet1!$B$3:$D$3"}, {Name: "Sheet1!$A$3", Categories: "Sheet1!$B$1:$D$1", Values: "Sheet1!$B$3:$D$3"},
{Name: "Sheet1!$A$4", Categories: "Sheet1!$B$1:$D$1", Values: "Sheet1!$B$4:$D$4"}, {Name: "Sheet1!$A$4", Categories: "Sheet1!$B$1:$D$1", Values: "Sheet1!$B$4:$D$4"},
} }
assert.NoError(t, f.AddChartSheet("Chart1", &Chart{Type: Col3DClustered, Series: series, Title: ChartTitle{Name: "Fruit 3D Clustered Column Chart"}})) assert.NoError(t, f.AddChartSheet("Chart1", &Chart{Type: Col3DClustered, Series: series, Title: []RichTextRun{{Text: "Fruit 3D Clustered Column Chart"}}}))
// Test set the chartsheet as active sheet // Test set the chartsheet as active sheet
var sheetIdx int var sheetIdx int
for idx, sheetName := range f.GetSheetList() { for idx, sheetName := range f.GetSheetList() {
@ -332,11 +346,11 @@ func TestAddChartSheet(t *testing.T) {
assert.EqualError(t, f.SetCellValue("Chart1", "A1", true), "sheet Chart1 is not a worksheet") assert.EqualError(t, f.SetCellValue("Chart1", "A1", true), "sheet Chart1 is not a worksheet")
// Test add chartsheet on already existing name sheet // Test add chartsheet on already existing name sheet
assert.EqualError(t, f.AddChartSheet("Sheet1", &Chart{Type: Col3DClustered, Series: series, Title: ChartTitle{Name: "Fruit 3D Clustered Column Chart"}}), ErrExistsSheet.Error()) assert.EqualError(t, f.AddChartSheet("Sheet1", &Chart{Type: Col3DClustered, Series: series, Title: []RichTextRun{{Text: "Fruit 3D Clustered Column Chart"}}}), ErrExistsSheet.Error())
// Test add chartsheet with invalid sheet name // Test add chartsheet with invalid sheet name
assert.EqualError(t, f.AddChartSheet("Sheet:1", nil, &Chart{Type: Col3DClustered, Series: series, Title: ChartTitle{Name: "Fruit 3D Clustered Column Chart"}}), ErrSheetNameInvalid.Error()) assert.EqualError(t, f.AddChartSheet("Sheet:1", nil, &Chart{Type: Col3DClustered, Series: series, Title: []RichTextRun{{Text: "Fruit 3D Clustered Column Chart"}}}), ErrSheetNameInvalid.Error())
// Test with unsupported chart type // Test with unsupported chart type
assert.EqualError(t, f.AddChartSheet("Chart2", &Chart{Type: 0x37, Series: series, Title: ChartTitle{Name: "Fruit 3D Clustered Column Chart"}}), newUnsupportedChartType(0x37).Error()) assert.EqualError(t, f.AddChartSheet("Chart2", &Chart{Type: 0x37, Series: series, Title: []RichTextRun{{Text: "Fruit 3D Clustered Column Chart"}}}), newUnsupportedChartType(0x37).Error())
assert.NoError(t, f.UpdateLinkedValue()) assert.NoError(t, f.UpdateLinkedValue())
@ -345,7 +359,7 @@ func TestAddChartSheet(t *testing.T) {
f = NewFile() f = NewFile()
f.ContentTypes = nil f.ContentTypes = nil
f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
assert.EqualError(t, f.AddChartSheet("Chart4", &Chart{Type: Col, Series: []ChartSeries{{Name: "Sheet1!$A$30", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$30:$D$30"}}, Title: ChartTitle{Name: "2D Column Chart"}}), "XML syntax error on line 1: invalid UTF-8") assert.EqualError(t, f.AddChartSheet("Chart4", &Chart{Type: Col, Series: []ChartSeries{{Name: "Sheet1!$A$30", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$30:$D$30"}}, Title: []RichTextRun{{Text: "2D Column Chart"}}}), "XML syntax error on line 1: invalid UTF-8")
} }
func TestDeleteChart(t *testing.T) { func TestDeleteChart(t *testing.T) {
@ -363,8 +377,8 @@ func TestDeleteChart(t *testing.T) {
{Name: "Sheet1!$A$37", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$37:$D$37"}, {Name: "Sheet1!$A$37", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$37:$D$37"},
} }
format := GraphicOptions{ format := GraphicOptions{
ScaleX: defaultPictureScale, ScaleX: defaultDrawingScale,
ScaleY: defaultPictureScale, ScaleY: defaultDrawingScale,
OffsetX: 15, OffsetX: 15,
OffsetY: 10, OffsetY: 10,
PrintObject: boolPtr(true), PrintObject: boolPtr(true),
@ -380,7 +394,7 @@ func TestDeleteChart(t *testing.T) {
ShowSerName: true, ShowSerName: true,
ShowVal: true, ShowVal: true,
} }
assert.NoError(t, f.AddChart("Sheet1", "P1", &Chart{Type: Col, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "2D Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"})) assert.NoError(t, f.AddChart("Sheet1", "P1", &Chart{Type: Col, Series: series, Format: format, Legend: legend, Title: []RichTextRun{{Text: "2D Column Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}))
assert.NoError(t, f.DeleteChart("Sheet1", "P1")) assert.NoError(t, f.DeleteChart("Sheet1", "P1"))
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDeleteChart.xlsx"))) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDeleteChart.xlsx")))
// Test delete chart with invalid sheet name // Test delete chart with invalid sheet name
@ -395,7 +409,7 @@ func TestDeleteChart(t *testing.T) {
} }
func TestChartWithLogarithmicBase(t *testing.T) { func TestChartWithLogarithmicBase(t *testing.T) {
// Create test XLSX file with data // Create test workbook with data
f := NewFile() f := NewFile()
sheet1 := f.GetSheetName(0) sheet1 := f.GetSheetName(0)
categories := map[string]float64{ categories := map[string]float64{
@ -429,25 +443,25 @@ func TestChartWithLogarithmicBase(t *testing.T) {
cell string cell string
opts *Chart opts *Chart
}{ }{
{cell: "C1", opts: &Chart{Type: Line, Dimension: ChartDimension{Width: dimension[0], Height: dimension[1]}, Series: series, Title: ChartTitle{Name: "Line chart without log scaling"}}}, {cell: "C1", opts: &Chart{Type: Line, Dimension: ChartDimension{Width: dimension[0], Height: dimension[1]}, Series: series, Title: []RichTextRun{{Text: "Line chart without log scaling"}}}},
{cell: "M1", opts: &Chart{Type: Line, Dimension: ChartDimension{Width: dimension[0], Height: dimension[1]}, Series: series, Title: ChartTitle{Name: "Line chart with log 10.5 scaling"}, YAxis: ChartAxis{LogBase: 10.5}}}, {cell: "M1", opts: &Chart{Type: Line, Dimension: ChartDimension{Width: dimension[0], Height: dimension[1]}, Series: series, Title: []RichTextRun{{Text: "Line chart with log 10.5 scaling"}}, YAxis: ChartAxis{LogBase: 10.5}}},
{cell: "A25", opts: &Chart{Type: Line, Dimension: ChartDimension{Width: dimension[2], Height: dimension[3]}, Series: series, Title: ChartTitle{Name: "Line chart with log 1.9 scaling"}, YAxis: ChartAxis{LogBase: 1.9}}}, {cell: "A25", opts: &Chart{Type: Line, Dimension: ChartDimension{Width: dimension[2], Height: dimension[3]}, Series: series, Title: []RichTextRun{{Text: "Line chart with log 1.9 scaling"}}, YAxis: ChartAxis{LogBase: 1.9}}},
{cell: "F25", opts: &Chart{Type: Line, Dimension: ChartDimension{Width: dimension[2], Height: dimension[3]}, Series: series, Title: ChartTitle{Name: "Line chart with log 2 scaling"}, YAxis: ChartAxis{LogBase: 2}}}, {cell: "F25", opts: &Chart{Type: Line, Dimension: ChartDimension{Width: dimension[2], Height: dimension[3]}, Series: series, Title: []RichTextRun{{Text: "Line chart with log 2 scaling"}}, YAxis: ChartAxis{LogBase: 2}}},
{cell: "K25", opts: &Chart{Type: Line, Dimension: ChartDimension{Width: dimension[2], Height: dimension[3]}, Series: series, Title: ChartTitle{Name: "Line chart with log 1000.1 scaling"}, YAxis: ChartAxis{LogBase: 1000.1}}}, {cell: "K25", opts: &Chart{Type: Line, Dimension: ChartDimension{Width: dimension[2], Height: dimension[3]}, Series: series, Title: []RichTextRun{{Text: "Line chart with log 1000.1 scaling"}}, YAxis: ChartAxis{LogBase: 1000.1}}},
{cell: "P25", opts: &Chart{Type: Line, Dimension: ChartDimension{Width: dimension[2], Height: dimension[3]}, Series: series, Title: ChartTitle{Name: "Line chart with log 1000 scaling"}, YAxis: ChartAxis{LogBase: 1000}}}, {cell: "P25", opts: &Chart{Type: Line, Dimension: ChartDimension{Width: dimension[2], Height: dimension[3]}, Series: series, Title: []RichTextRun{{Text: "Line chart with log 1000 scaling"}}, YAxis: ChartAxis{LogBase: 1000}}},
} { } {
// Add two chart, one without and one with log scaling // Add two chart, one without and one with log scaling
assert.NoError(t, f.AddChart(sheet1, c.cell, c.opts)) assert.NoError(t, f.AddChart(sheet1, c.cell, c.opts))
} }
// Export XLSX file for human confirmation // Export workbook for human confirmation
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestChartWithLogarithmicBase10.xlsx"))) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestChartWithLogarithmicBase10.xlsx")))
// Write the XLSX file to a buffer // Write the workbook to a buffer
var buffer bytes.Buffer var buffer bytes.Buffer
assert.NoError(t, f.Write(&buffer)) assert.NoError(t, f.Write(&buffer))
// Read back the XLSX file from the buffer // Read back the workbook from the buffer
newFile, err := OpenReader(&buffer) newFile, err := OpenReader(&buffer)
assert.NoError(t, err) assert.NoError(t, err)

143
col.go
View File

@ -1,4 +1,4 @@
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in // this source code is governed by a BSD-style license that can be found in
// the LICENSE file. // the LICENSE file.
// //
@ -7,7 +7,7 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming // Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of // API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.16 or later. // data. This library needs Go version 1.18 or later.
package excelize package excelize
@ -18,7 +18,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/mohae/deepcopy" "github.com/tiendc/go-deepcopy"
) )
// Define the default cell size and EMU unit of measurement. // Define the default cell size and EMU unit of measurement.
@ -92,7 +92,7 @@ func (cols *Cols) Rows(opts ...Options) ([]string, error) {
if cols.stashCol >= cols.curCol { if cols.stashCol >= cols.curCol {
return rowIterator.cells, rowIterator.err return rowIterator.cells, rowIterator.err
} }
cols.rawCellValue = getOptions(opts...).RawCellValue cols.rawCellValue = cols.f.getOptions(opts...).RawCellValue
if cols.sst, rowIterator.err = cols.f.sharedStringsReader(); rowIterator.err != nil { if cols.sst, rowIterator.err = cols.f.sharedStringsReader(); rowIterator.err != nil {
return rowIterator.cells, rowIterator.err return rowIterator.cells, rowIterator.err
} }
@ -215,11 +215,11 @@ func (f *File) Cols(sheet string) (*Cols, error) {
if !ok { if !ok {
return nil, ErrSheetNotExist{sheet} return nil, ErrSheetNotExist{sheet}
} }
if ws, ok := f.Sheet.Load(name); ok && ws != nil { if worksheet, ok := f.Sheet.Load(name); ok && worksheet != nil {
worksheet := ws.(*xlsxWorksheet) ws := worksheet.(*xlsxWorksheet)
worksheet.Lock() ws.mu.Lock()
defer worksheet.Unlock() defer ws.mu.Unlock()
output, _ := xml.Marshal(worksheet) output, _ := xml.Marshal(ws)
f.saveFileList(name, f.replaceNameSpaceBytes(name, output)) f.saveFileList(name, f.replaceNameSpaceBytes(name, output))
} }
var colIterator columnXMLIterator var colIterator columnXMLIterator
@ -257,12 +257,15 @@ func (f *File) GetColVisible(sheet, col string) (bool, error) {
if err != nil { if err != nil {
return true, err return true, err
} }
f.mu.Lock()
ws, err := f.workSheetReader(sheet) ws, err := f.workSheetReader(sheet)
if err != nil { if err != nil {
f.mu.Unlock()
return false, err return false, err
} }
ws.Lock() f.mu.Unlock()
defer ws.Unlock() ws.mu.Lock()
defer ws.mu.Unlock()
if ws.Cols == nil { if ws.Cols == nil {
return true, err return true, err
} }
@ -287,7 +290,7 @@ func (f *File) GetColVisible(sheet, col string) (bool, error) {
// //
// err := f.SetColVisible("Sheet1", "D:F", false) // err := f.SetColVisible("Sheet1", "D:F", false)
func (f *File) SetColVisible(sheet, columns string, visible bool) error { func (f *File) SetColVisible(sheet, columns string, visible bool) error {
min, max, err := f.parseColRange(columns) minVal, maxVal, err := f.parseColRange(columns)
if err != nil { if err != nil {
return err return err
} }
@ -295,11 +298,11 @@ func (f *File) SetColVisible(sheet, columns string, visible bool) error {
if err != nil { if err != nil {
return err return err
} }
ws.Lock() ws.mu.Lock()
defer ws.Unlock() defer ws.mu.Unlock()
colData := xlsxCol{ colData := xlsxCol{
Min: min, Min: minVal,
Max: max, Max: maxVal,
Width: float64Ptr(defaultColWidth), Width: float64Ptr(defaultColWidth),
Hidden: !visible, Hidden: !visible,
CustomWidth: true, CustomWidth: true,
@ -351,20 +354,20 @@ func (f *File) GetColOutlineLevel(sheet, col string) (uint8, error) {
} }
// parseColRange parse and convert column range with column name to the column number. // parseColRange parse and convert column range with column name to the column number.
func (f *File) parseColRange(columns string) (min, max int, err error) { func (f *File) parseColRange(columns string) (minVal, maxVal int, err error) {
colsTab := strings.Split(columns, ":") colsTab := strings.Split(columns, ":")
min, err = ColumnNameToNumber(colsTab[0]) minVal, err = ColumnNameToNumber(colsTab[0])
if err != nil { if err != nil {
return return
} }
max = min maxVal = minVal
if len(colsTab) == 2 { if len(colsTab) == 2 {
if max, err = ColumnNameToNumber(colsTab[1]); err != nil { if maxVal, err = ColumnNameToNumber(colsTab[1]); err != nil {
return return
} }
} }
if max < min { if maxVal < minVal {
min, max = max, min minVal, maxVal = maxVal, minVal
} }
return return
} }
@ -424,32 +427,40 @@ func (f *File) SetColOutlineLevel(sheet, col string, level uint8) error {
// //
// err = f.SetColStyle("Sheet1", "C:F", style) // err = f.SetColStyle("Sheet1", "C:F", style)
func (f *File) SetColStyle(sheet, columns string, styleID int) error { func (f *File) SetColStyle(sheet, columns string, styleID int) error {
min, max, err := f.parseColRange(columns) minVal, maxVal, err := f.parseColRange(columns)
if err != nil { if err != nil {
return err return err
} }
f.mu.Lock()
s, err := f.stylesReader() s, err := f.stylesReader()
if err != nil { if err != nil {
f.mu.Unlock()
return err return err
} }
s.Lock()
if styleID < 0 || s.CellXfs == nil || len(s.CellXfs.Xf) <= styleID {
s.Unlock()
return newInvalidStyleID(styleID)
}
s.Unlock()
ws, err := f.workSheetReader(sheet) ws, err := f.workSheetReader(sheet)
if err != nil { if err != nil {
f.mu.Unlock()
return err return err
} }
ws.Lock() f.mu.Unlock()
s.mu.Lock()
if styleID < 0 || s.CellXfs == nil || len(s.CellXfs.Xf) <= styleID {
s.mu.Unlock()
return newInvalidStyleID(styleID)
}
s.mu.Unlock()
ws.mu.Lock()
if ws.Cols == nil { if ws.Cols == nil {
ws.Cols = &xlsxCols{} ws.Cols = &xlsxCols{}
} }
width := defaultColWidth
if ws.SheetFormatPr != nil && ws.SheetFormatPr.DefaultColWidth > 0 {
width = ws.SheetFormatPr.DefaultColWidth
}
ws.Cols.Col = flatCols(xlsxCol{ ws.Cols.Col = flatCols(xlsxCol{
Min: min, Min: minVal,
Max: max, Max: maxVal,
Width: float64Ptr(defaultColWidth), Width: float64Ptr(width),
Style: styleID, Style: styleID,
}, ws.Cols.Col, func(fc, c xlsxCol) xlsxCol { }, ws.Cols.Col, func(fc, c xlsxCol) xlsxCol {
fc.BestFit = c.BestFit fc.BestFit = c.BestFit
@ -461,9 +472,9 @@ func (f *File) SetColStyle(sheet, columns string, styleID int) error {
fc.Width = c.Width fc.Width = c.Width
return fc return fc
}) })
ws.Unlock() ws.mu.Unlock()
if rows := len(ws.SheetData.Row); rows > 0 { if rows := len(ws.SheetData.Row); rows > 0 {
for col := min; col <= max; col++ { for col := minVal; col <= maxVal; col++ {
from, _ := CoordinatesToCellName(col, 1) from, _ := CoordinatesToCellName(col, 1)
to, _ := CoordinatesToCellName(col, rows) to, _ := CoordinatesToCellName(col, rows)
err = f.SetCellStyle(sheet, from, to, styleID) err = f.SetCellStyle(sheet, from, to, styleID)
@ -477,22 +488,25 @@ func (f *File) SetColStyle(sheet, columns string, styleID int) error {
// //
// err := f.SetColWidth("Sheet1", "A", "H", 20) // err := f.SetColWidth("Sheet1", "A", "H", 20)
func (f *File) SetColWidth(sheet, startCol, endCol string, width float64) error { func (f *File) SetColWidth(sheet, startCol, endCol string, width float64) error {
min, max, err := f.parseColRange(startCol + ":" + endCol) minVal, maxVal, err := f.parseColRange(startCol + ":" + endCol)
if err != nil { if err != nil {
return err return err
} }
if width > MaxColumnWidth { if width > MaxColumnWidth {
return ErrColumnWidth return ErrColumnWidth
} }
f.mu.Lock()
ws, err := f.workSheetReader(sheet) ws, err := f.workSheetReader(sheet)
if err != nil { if err != nil {
f.mu.Unlock()
return err return err
} }
ws.Lock() f.mu.Unlock()
defer ws.Unlock() ws.mu.Lock()
defer ws.mu.Unlock()
col := xlsxCol{ col := xlsxCol{
Min: min, Min: minVal,
Max: max, Max: maxVal,
Width: float64Ptr(width), Width: float64Ptr(width),
CustomWidth: true, CustomWidth: true,
} }
@ -519,7 +533,8 @@ func (f *File) SetColWidth(sheet, startCol, endCol string, width float64) error
func flatCols(col xlsxCol, cols []xlsxCol, replacer func(fc, c xlsxCol) xlsxCol) []xlsxCol { func flatCols(col xlsxCol, cols []xlsxCol, replacer func(fc, c xlsxCol) xlsxCol) []xlsxCol {
var fc []xlsxCol var fc []xlsxCol
for i := col.Min; i <= col.Max; i++ { for i := col.Min; i <= col.Max; i++ {
c := deepcopy.Copy(col).(xlsxCol) var c xlsxCol
deepcopy.Copy(&c, col)
c.Min, c.Max = i, i c.Min, c.Max = i, i
fc = append(fc, c) fc = append(fc, c)
} }
@ -537,7 +552,8 @@ func flatCols(col xlsxCol, cols []xlsxCol, replacer func(fc, c xlsxCol) xlsxCol)
fc[idx] = replacer(fc[idx], column) fc[idx] = replacer(fc[idx], column)
continue continue
} }
c := deepcopy.Copy(column).(xlsxCol) var c xlsxCol
deepcopy.Copy(&c, column)
c.Min, c.Max = i, i c.Min, c.Max = i, i
fc = append(fc, c) fc = append(fc, c)
} }
@ -594,20 +610,21 @@ func flatCols(col xlsxCol, cols []xlsxCol, replacer func(fc, c xlsxCol) xlsxCol)
// width # Width of object frame. // width # Width of object frame.
// height # Height of object frame. // height # Height of object frame.
func (f *File) positionObjectPixels(sheet string, col, row, x1, y1, width, height int) (int, int, int, int, int, int) { func (f *File) positionObjectPixels(sheet string, col, row, x1, y1, width, height int) (int, int, int, int, int, int) {
colIdx, rowIdx := col-1, row-1
// Adjust start column for offsets that are greater than the col width. // Adjust start column for offsets that are greater than the col width.
for x1 >= f.getColWidth(sheet, col) { for x1 >= f.getColWidth(sheet, colIdx+1) {
x1 -= f.getColWidth(sheet, col) colIdx++
col++ x1 -= f.getColWidth(sheet, colIdx)
} }
// Adjust start row for offsets that are greater than the row height. // Adjust start row for offsets that are greater than the row height.
for y1 >= f.getRowHeight(sheet, row) { for y1 >= f.getRowHeight(sheet, rowIdx+1) {
y1 -= f.getRowHeight(sheet, row) rowIdx++
row++ y1 -= f.getRowHeight(sheet, rowIdx)
} }
// Initialized end cell to the same as the start cell. // Initialized end cell to the same as the start cell.
colEnd, rowEnd := col, row colEnd, rowEnd := colIdx, rowIdx
width += x1 width += x1
height += y1 height += y1
@ -625,17 +642,15 @@ func (f *File) positionObjectPixels(sheet string, col, row, x1, y1, width, heigh
} }
// The end vertices are whatever is left from the width and height. // The end vertices are whatever is left from the width and height.
x2 := width return colIdx, rowIdx, colEnd, rowEnd, width, height
y2 := height
return col, row, colEnd, rowEnd, x2, y2
} }
// getColWidth provides a function to get column width in pixels by given // getColWidth provides a function to get column width in pixels by given
// sheet name and column number. // sheet name and column number.
func (f *File) getColWidth(sheet string, col int) int { func (f *File) getColWidth(sheet string, col int) int {
ws, _ := f.workSheetReader(sheet) ws, _ := f.workSheetReader(sheet)
ws.Lock() ws.mu.Lock()
defer ws.Unlock() defer ws.mu.Unlock()
if ws.Cols != nil { if ws.Cols != nil {
var width float64 var width float64
for _, v := range ws.Cols.Col { for _, v := range ws.Cols.Col {
@ -647,6 +662,9 @@ func (f *File) getColWidth(sheet string, col int) int {
return int(convertColWidthToPixels(width)) return int(convertColWidthToPixels(width))
} }
} }
if ws.SheetFormatPr != nil && ws.SheetFormatPr.DefaultColWidth > 0 {
return int(convertColWidthToPixels(ws.SheetFormatPr.DefaultColWidth))
}
// Optimization for when the column widths haven't changed. // Optimization for when the column widths haven't changed.
return int(defaultColWidthPixels) return int(defaultColWidthPixels)
} }
@ -659,12 +677,15 @@ func (f *File) GetColStyle(sheet, col string) (int, error) {
if err != nil { if err != nil {
return styleID, err return styleID, err
} }
f.mu.Lock()
ws, err := f.workSheetReader(sheet) ws, err := f.workSheetReader(sheet)
if err != nil { if err != nil {
f.mu.Unlock()
return styleID, err return styleID, err
} }
ws.Lock() f.mu.Unlock()
defer ws.Unlock() ws.mu.Lock()
defer ws.mu.Unlock()
if ws.Cols != nil { if ws.Cols != nil {
for _, v := range ws.Cols.Col { for _, v := range ws.Cols.Col {
if v.Min <= colNum && colNum <= v.Max { if v.Min <= colNum && colNum <= v.Max {
@ -682,12 +703,15 @@ func (f *File) GetColWidth(sheet, col string) (float64, error) {
if err != nil { if err != nil {
return defaultColWidth, err return defaultColWidth, err
} }
f.mu.Lock()
ws, err := f.workSheetReader(sheet) ws, err := f.workSheetReader(sheet)
if err != nil { if err != nil {
f.mu.Unlock()
return defaultColWidth, err return defaultColWidth, err
} }
ws.Lock() f.mu.Unlock()
defer ws.Unlock() ws.mu.Lock()
defer ws.mu.Unlock()
if ws.Cols != nil { if ws.Cols != nil {
var width float64 var width float64
for _, v := range ws.Cols.Col { for _, v := range ws.Cols.Col {
@ -699,6 +723,9 @@ func (f *File) GetColWidth(sheet, col string) (float64, error) {
return width, err return width, err
} }
} }
if ws.SheetFormatPr != nil && ws.SheetFormatPr.DefaultColWidth > 0 {
return ws.SheetFormatPr.DefaultColWidth, err
}
// Optimization for when the column widths haven't changed. // Optimization for when the column widths haven't changed.
return defaultColWidth, err return defaultColWidth, err
} }

View File

@ -1,7 +1,9 @@
package excelize package excelize
import ( import (
"fmt"
"path/filepath" "path/filepath"
"sync"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -71,6 +73,17 @@ func TestCols(t *testing.T) {
cols.Next() cols.Next()
_, err = cols.Rows() _, err = cols.Rows()
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
f = NewFile()
f.Sheet.Delete("xl/worksheets/sheet1.xml")
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`<worksheet><sheetData><row r="A"><c r="2" t="inlineStr"><is><t>B</t></is></c></row></sheetData></worksheet>`))
f.checked = sync.Map{}
_, err = f.Cols("Sheet1")
assert.EqualError(t, err, `strconv.Atoi: parsing "A": invalid syntax`)
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`<worksheet><sheetData><row r="2"><c r="A" t="inlineStr"><is><t>B</t></is></c></row></sheetData></worksheet>`))
_, err = f.Cols("Sheet1")
assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
} }
func TestColumnsIterator(t *testing.T) { func TestColumnsIterator(t *testing.T) {
@ -124,12 +137,12 @@ func TestGetColsError(t *testing.T) {
f = NewFile() f = NewFile()
f.Sheet.Delete("xl/worksheets/sheet1.xml") f.Sheet.Delete("xl/worksheets/sheet1.xml")
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`<worksheet><sheetData><row r="A"><c r="2" t="inlineStr"><is><t>B</t></is></c></row></sheetData></worksheet>`)) f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(`<worksheet xmlns="%s"><sheetData><row r="A"><c r="2" t="inlineStr"><is><t>B</t></is></c></row></sheetData></worksheet>`, NameSpaceSpreadSheet.Value)))
f.checked = nil f.checked = sync.Map{}
_, err = f.GetCols("Sheet1") _, err = f.GetCols("Sheet1")
assert.EqualError(t, err, `strconv.Atoi: parsing "A": invalid syntax`) assert.EqualError(t, err, `strconv.Atoi: parsing "A": invalid syntax`)
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`<worksheet><sheetData><row r="2"><c r="A" t="inlineStr"><is><t>B</t></is></c></row></sheetData></worksheet>`)) f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(`<worksheet xmlns="%s"><sheetData><row r="2"><c r="A" t="inlineStr"><is><t>B</t></is></c></row></sheetData></worksheet>`, NameSpaceSpreadSheet.Value)))
_, err = f.GetCols("Sheet1") _, err = f.GetCols("Sheet1")
assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
@ -139,7 +152,7 @@ func TestGetColsError(t *testing.T) {
cols.totalRows = 2 cols.totalRows = 2
cols.totalCols = 2 cols.totalCols = 2
cols.curCol = 1 cols.curCol = 1
cols.sheetXML = []byte(`<worksheet><sheetData><row r="1"><c r="A" t="inlineStr"><is><t>A</t></is></c></row></sheetData></worksheet>`) cols.sheetXML = []byte(fmt.Sprintf(`<worksheet xmlns="%s"><sheetData><row r="1"><c r="A" t="inlineStr"><is><t>A</t></is></c></row></sheetData></worksheet>`, NameSpaceSpreadSheet.Value))
_, err = cols.Rows() _, err = cols.Rows()
assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
@ -217,7 +230,7 @@ func TestColumnVisibility(t *testing.T) {
assert.Equal(t, true, visible) assert.Equal(t, true, visible)
assert.NoError(t, err) assert.NoError(t, err)
// Test get column visible on an inexistent worksheet // Test get column visible on not exists worksheet
_, err = f.GetColVisible("SheetN", "F") _, err = f.GetColVisible("SheetN", "F")
assert.EqualError(t, err, "sheet SheetN does not exist") assert.EqualError(t, err, "sheet SheetN does not exist")
// Test get column visible with invalid sheet name // Test get column visible with invalid sheet name
@ -353,6 +366,16 @@ func TestSetColStyle(t *testing.T) {
f.Styles = nil f.Styles = nil
f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset) f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset)
assert.EqualError(t, f.SetColStyle("Sheet1", "C:F", styleID), "XML syntax error on line 1: invalid UTF-8") assert.EqualError(t, f.SetColStyle("Sheet1", "C:F", styleID), "XML syntax error on line 1: invalid UTF-8")
// Test set column style with worksheet properties columns default width settings
f = NewFile()
assert.NoError(t, f.SetSheetProps("Sheet1", &SheetPropsOptions{DefaultColWidth: float64Ptr(20)}))
style, err = f.NewStyle(&Style{Alignment: &Alignment{Vertical: "center"}})
assert.NoError(t, err)
assert.NoError(t, f.SetColStyle("Sheet1", "A:Z", style))
width, err := f.GetColWidth("Sheet1", "B")
assert.NoError(t, err)
assert.Equal(t, 20.0, width)
} }
func TestColWidth(t *testing.T) { func TestColWidth(t *testing.T) {
@ -366,6 +389,15 @@ func TestColWidth(t *testing.T) {
assert.Equal(t, defaultColWidth, width) assert.Equal(t, defaultColWidth, width)
assert.NoError(t, err) assert.NoError(t, err)
ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
assert.True(t, ok)
ws.(*xlsxWorksheet).SheetFormatPr = &xlsxSheetFormatPr{DefaultColWidth: 10}
ws.(*xlsxWorksheet).Cols = nil
width, err = f.GetColWidth("Sheet1", "A")
assert.NoError(t, err)
assert.Equal(t, 10.0, width)
assert.Equal(t, 76, f.getColWidth("Sheet1", 1))
// Test set and get column width with illegal cell reference // Test set and get column width with illegal cell reference
width, err = f.GetColWidth("Sheet1", "*") width, err = f.GetColWidth("Sheet1", "*")
assert.Equal(t, defaultColWidth, width) assert.Equal(t, defaultColWidth, width)
@ -407,7 +439,7 @@ func TestInsertCols(t *testing.T) {
f := NewFile() f := NewFile()
sheet1 := f.GetSheetName(0) sheet1 := f.GetSheetName(0)
fillCells(f, sheet1, 10, 10) assert.NoError(t, fillCells(f, sheet1, 10, 10))
assert.NoError(t, f.SetCellHyperLink(sheet1, "A5", "https://github.com/xuri/excelize", "External")) assert.NoError(t, f.SetCellHyperLink(sheet1, "A5", "https://github.com/xuri/excelize", "External"))
assert.NoError(t, f.MergeCell(sheet1, "A1", "C3")) assert.NoError(t, f.MergeCell(sheet1, "A1", "C3"))
@ -430,7 +462,7 @@ func TestRemoveCol(t *testing.T) {
f := NewFile() f := NewFile()
sheet1 := f.GetSheetName(0) sheet1 := f.GetSheetName(0)
fillCells(f, sheet1, 10, 15) assert.NoError(t, fillCells(f, sheet1, 10, 15))
assert.NoError(t, f.SetCellHyperLink(sheet1, "A5", "https://github.com/xuri/excelize", "External")) assert.NoError(t, f.SetCellHyperLink(sheet1, "A5", "https://github.com/xuri/excelize", "External"))
assert.NoError(t, f.SetCellHyperLink(sheet1, "C5", "https://github.com", "External")) assert.NoError(t, f.SetCellHyperLink(sheet1, "C5", "https://github.com", "External"))

View File

@ -1,426 +0,0 @@
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
// Package excelize providing a set of functions that allow you to write to and
// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.16 or later.
package excelize
import (
"bytes"
"encoding/xml"
"fmt"
"io"
"path/filepath"
"strconv"
"strings"
)
// GetComments retrieves all comments in a worksheet by given worksheet name.
func (f *File) GetComments(sheet string) ([]Comment, error) {
var comments []Comment
sheetXMLPath, ok := f.getSheetXMLPath(sheet)
if !ok {
return comments, newNoExistSheetError(sheet)
}
commentsXML := f.getSheetComments(filepath.Base(sheetXMLPath))
if !strings.HasPrefix(commentsXML, "/") {
commentsXML = "xl" + strings.TrimPrefix(commentsXML, "..")
}
commentsXML = strings.TrimPrefix(commentsXML, "/")
cmts, err := f.commentsReader(commentsXML)
if err != nil {
return comments, err
}
if cmts != nil {
for _, cmt := range cmts.CommentList.Comment {
comment := Comment{}
if cmt.AuthorID < len(cmts.Authors.Author) {
comment.Author = cmts.Authors.Author[cmt.AuthorID]
}
comment.Cell = cmt.Ref
comment.AuthorID = cmt.AuthorID
if cmt.Text.T != nil {
comment.Text += *cmt.Text.T
}
for _, text := range cmt.Text.R {
if text.T != nil {
run := RichTextRun{Text: text.T.Val}
if text.RPr != nil {
run.Font = newFont(text.RPr)
}
comment.Runs = append(comment.Runs, run)
}
}
comments = append(comments, comment)
}
}
return comments, nil
}
// getSheetComments provides the method to get the target comment reference by
// given worksheet file path.
func (f *File) getSheetComments(sheetFile string) string {
rels, _ := f.relsReader("xl/worksheets/_rels/" + sheetFile + ".rels")
if sheetRels := rels; sheetRels != nil {
sheetRels.Lock()
defer sheetRels.Unlock()
for _, v := range sheetRels.Relationships {
if v.Type == SourceRelationshipComments {
return v.Target
}
}
}
return ""
}
// AddComment provides the method to add comment in a sheet by given worksheet
// index, cell and format set (such as author and text). Note that the max
// author length is 255 and the max text length is 32512. For example, add a
// comment in Sheet1!$A$30:
//
// err := f.AddComment("Sheet1", excelize.Comment{
// Cell: "A12",
// Author: "Excelize",
// Runs: []excelize.RichTextRun{
// {Text: "Excelize: ", Font: &excelize.Font{Bold: true}},
// {Text: "This is a comment."},
// },
// })
func (f *File) AddComment(sheet string, comment Comment) error {
// Read sheet data.
ws, err := f.workSheetReader(sheet)
if err != nil {
return err
}
commentID := f.countComments() + 1
drawingVML := "xl/drawings/vmlDrawing" + strconv.Itoa(commentID) + ".vml"
sheetRelationshipsComments := "../comments" + strconv.Itoa(commentID) + ".xml"
sheetRelationshipsDrawingVML := "../drawings/vmlDrawing" + strconv.Itoa(commentID) + ".vml"
if ws.LegacyDrawing != nil {
// The worksheet already has a comments relationships, use the relationships drawing ../drawings/vmlDrawing%d.vml.
sheetRelationshipsDrawingVML = f.getSheetRelationshipsTargetByID(sheet, ws.LegacyDrawing.RID)
commentID, _ = strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(sheetRelationshipsDrawingVML, "../drawings/vmlDrawing"), ".vml"))
drawingVML = strings.ReplaceAll(sheetRelationshipsDrawingVML, "..", "xl")
} else {
// Add first comment for given sheet.
sheetXMLPath, _ := f.getSheetXMLPath(sheet)
sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetXMLPath, "xl/worksheets/") + ".rels"
rID := f.addRels(sheetRels, SourceRelationshipDrawingVML, sheetRelationshipsDrawingVML, "")
f.addRels(sheetRels, SourceRelationshipComments, sheetRelationshipsComments, "")
f.addSheetNameSpace(sheet, SourceRelationship)
f.addSheetLegacyDrawing(sheet, rID)
}
commentsXML := "xl/comments" + strconv.Itoa(commentID) + ".xml"
var rows, cols int
for _, runs := range comment.Runs {
for _, subStr := range strings.Split(runs.Text, "\n") {
rows++
if chars := len(subStr); chars > cols {
cols = chars
}
}
}
if err = f.addDrawingVML(commentID, drawingVML, comment.Cell, rows+1, cols); err != nil {
return err
}
if err = f.addComment(commentsXML, comment); err != nil {
return err
}
return f.addContentTypePart(commentID, "comments")
}
// DeleteComment provides the method to delete comment in a sheet by given
// worksheet name. For example, delete the comment in Sheet1!$A$30:
//
// err := f.DeleteComment("Sheet1", "A30")
func (f *File) DeleteComment(sheet, cell string) error {
if err := checkSheetName(sheet); err != nil {
return err
}
sheetXMLPath, ok := f.getSheetXMLPath(sheet)
if !ok {
return newNoExistSheetError(sheet)
}
commentsXML := f.getSheetComments(filepath.Base(sheetXMLPath))
if !strings.HasPrefix(commentsXML, "/") {
commentsXML = "xl" + strings.TrimPrefix(commentsXML, "..")
}
commentsXML = strings.TrimPrefix(commentsXML, "/")
cmts, err := f.commentsReader(commentsXML)
if err != nil {
return err
}
if cmts != nil {
for i := 0; i < len(cmts.CommentList.Comment); i++ {
cmt := cmts.CommentList.Comment[i]
if cmt.Ref != cell {
continue
}
if len(cmts.CommentList.Comment) > 1 {
cmts.CommentList.Comment = append(
cmts.CommentList.Comment[:i],
cmts.CommentList.Comment[i+1:]...,
)
i--
continue
}
cmts.CommentList.Comment = nil
}
f.Comments[commentsXML] = cmts
}
return err
}
// addDrawingVML provides a function to create comment as
// xl/drawings/vmlDrawing%d.vml by given commit ID and cell.
func (f *File) addDrawingVML(commentID int, drawingVML, cell string, lineCount, colCount int) error {
col, row, err := CellNameToCoordinates(cell)
if err != nil {
return err
}
yAxis := col - 1
xAxis := row - 1
vml := f.VMLDrawing[drawingVML]
if vml == nil {
vml = &vmlDrawing{
XMLNSv: "urn:schemas-microsoft-com:vml",
XMLNSo: "urn:schemas-microsoft-com:office:office",
XMLNSx: "urn:schemas-microsoft-com:office:excel",
XMLNSmv: "http://macVmlSchemaUri",
Shapelayout: &xlsxShapelayout{
Ext: "edit",
IDmap: &xlsxIDmap{
Ext: "edit",
Data: commentID,
},
},
Shapetype: &xlsxShapetype{
ID: "_x0000_t202",
Coordsize: "21600,21600",
Spt: 202,
Path: "m0,0l0,21600,21600,21600,21600,0xe",
Stroke: &xlsxStroke{
Joinstyle: "miter",
},
VPath: &vPath{
Gradientshapeok: "t",
Connecttype: "rect",
},
},
}
// load exist comment shapes from xl/drawings/vmlDrawing%d.vml
d, err := f.decodeVMLDrawingReader(drawingVML)
if err != nil {
return err
}
if d != nil {
for _, v := range d.Shape {
s := xlsxShape{
ID: "_x0000_s1025",
Type: "#_x0000_t202",
Style: "position:absolute;73.5pt;width:108pt;height:59.25pt;z-index:1;visibility:hidden",
Fillcolor: "#FBF6D6",
Strokecolor: "#EDEAA1",
Val: v.Val,
}
vml.Shape = append(vml.Shape, s)
}
}
}
sp := encodeShape{
Fill: &vFill{
Color2: "#FBFE82",
Angle: -180,
Type: "gradient",
Fill: &oFill{
Ext: "view",
Type: "gradientUnscaled",
},
},
Shadow: &vShadow{
On: "t",
Color: "black",
Obscured: "t",
},
Path: &vPath{
Connecttype: "none",
},
Textbox: &vTextbox{
Style: "mso-direction-alt:auto",
Div: &xlsxDiv{
Style: "text-align:left",
},
},
ClientData: &xClientData{
ObjectType: "Note",
Anchor: fmt.Sprintf(
"%d, 23, %d, 0, %d, %d, %d, 5",
1+yAxis, 1+xAxis, 2+yAxis+lineCount, colCount+yAxis, 2+xAxis+lineCount),
AutoFill: "True",
Row: xAxis,
Column: yAxis,
},
}
s, _ := xml.Marshal(sp)
shape := xlsxShape{
ID: "_x0000_s1025",
Type: "#_x0000_t202",
Style: "position:absolute;73.5pt;width:108pt;height:59.25pt;z-index:1;visibility:hidden",
Fillcolor: "#FBF6D6",
Strokecolor: "#EDEAA1",
Val: string(s[13 : len(s)-14]),
}
vml.Shape = append(vml.Shape, shape)
f.VMLDrawing[drawingVML] = vml
return err
}
// addComment provides a function to create chart as xl/comments%d.xml by
// given cell and format sets.
func (f *File) addComment(commentsXML string, comment Comment) error {
if comment.Author == "" {
comment.Author = "Author"
}
if len(comment.Author) > MaxFieldLength {
comment.Author = comment.Author[:MaxFieldLength]
}
cmts, err := f.commentsReader(commentsXML)
if err != nil {
return err
}
var authorID int
if cmts == nil {
cmts = &xlsxComments{Authors: xlsxAuthor{Author: []string{comment.Author}}}
}
if inStrSlice(cmts.Authors.Author, comment.Author, true) == -1 {
cmts.Authors.Author = append(cmts.Authors.Author, comment.Author)
authorID = len(cmts.Authors.Author) - 1
}
defaultFont, err := f.GetDefaultFont()
if err != nil {
return err
}
chars, cmt := 0, xlsxComment{
Ref: comment.Cell,
AuthorID: authorID,
Text: xlsxText{R: []xlsxR{}},
}
if comment.Text != "" {
if len(comment.Text) > TotalCellChars {
comment.Text = comment.Text[:TotalCellChars]
}
cmt.Text.T = stringPtr(comment.Text)
chars += len(comment.Text)
}
for _, run := range comment.Runs {
if chars == TotalCellChars {
break
}
if chars+len(run.Text) > TotalCellChars {
run.Text = run.Text[:TotalCellChars-chars]
}
chars += len(run.Text)
r := xlsxR{
RPr: &xlsxRPr{
Sz: &attrValFloat{Val: float64Ptr(9)},
Color: &xlsxColor{
Indexed: 81,
},
RFont: &attrValString{Val: stringPtr(defaultFont)},
Family: &attrValInt{Val: intPtr(2)},
},
T: &xlsxT{Val: run.Text, Space: xml.Attr{
Name: xml.Name{Space: NameSpaceXML, Local: "space"},
Value: "preserve",
}},
}
if run.Font != nil {
r.RPr = newRpr(run.Font)
}
cmt.Text.R = append(cmt.Text.R, r)
}
cmts.CommentList.Comment = append(cmts.CommentList.Comment, cmt)
f.Comments[commentsXML] = cmts
return err
}
// countComments provides a function to get comments files count storage in
// the folder xl.
func (f *File) countComments() int {
c1, c2 := 0, 0
f.Pkg.Range(func(k, v interface{}) bool {
if strings.Contains(k.(string), "xl/comments") {
c1++
}
return true
})
for rel := range f.Comments {
if strings.Contains(rel, "xl/comments") {
c2++
}
}
if c1 < c2 {
return c2
}
return c1
}
// decodeVMLDrawingReader provides a function to get the pointer to the
// structure after deserialization of xl/drawings/vmlDrawing%d.xml.
func (f *File) decodeVMLDrawingReader(path string) (*decodeVmlDrawing, error) {
if f.DecodeVMLDrawing[path] == nil {
c, ok := f.Pkg.Load(path)
if ok && c != nil {
f.DecodeVMLDrawing[path] = new(decodeVmlDrawing)
if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(c.([]byte)))).
Decode(f.DecodeVMLDrawing[path]); err != nil && err != io.EOF {
return nil, err
}
}
}
return f.DecodeVMLDrawing[path], nil
}
// vmlDrawingWriter provides a function to save xl/drawings/vmlDrawing%d.xml
// after serialize structure.
func (f *File) vmlDrawingWriter() {
for path, vml := range f.VMLDrawing {
if vml != nil {
v, _ := xml.Marshal(vml)
f.Pkg.Store(path, v)
}
}
}
// commentsReader provides a function to get the pointer to the structure
// after deserialization of xl/comments%d.xml.
func (f *File) commentsReader(path string) (*xlsxComments, error) {
if f.Comments[path] == nil {
content, ok := f.Pkg.Load(path)
if ok && content != nil {
f.Comments[path] = new(xlsxComments)
if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(content.([]byte)))).
Decode(f.Comments[path]); err != nil && err != io.EOF {
return nil, err
}
}
}
return f.Comments[path], nil
}
// commentsWriter provides a function to save xl/comments%d.xml after
// serialize structure.
func (f *File) commentsWriter() {
for path, c := range f.Comments {
if c != nil {
v, _ := xml.Marshal(c)
f.saveFileList(path, v)
}
}
}

View File

@ -1,146 +0,0 @@
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
// Package excelize providing a set of functions that allow you to write to and
// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.16 or later.
package excelize
import (
"encoding/xml"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestAddComment(t *testing.T) {
f, err := prepareTestBook1()
if !assert.NoError(t, err) {
t.FailNow()
}
s := strings.Repeat("c", TotalCellChars+1)
assert.NoError(t, f.AddComment("Sheet1", Comment{Cell: "A30", Author: s, Text: s, Runs: []RichTextRun{{Text: s}, {Text: s}}}))
assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "B7", Author: "Excelize", Text: s[:TotalCellChars-1], Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment."}}}))
// Test add comment on not exists worksheet
assert.EqualError(t, f.AddComment("SheetN", Comment{Cell: "B7", Author: "Excelize", Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment."}}}), "sheet SheetN does not exist")
// Test add comment on with illegal cell reference
assert.EqualError(t, f.AddComment("Sheet1", Comment{Cell: "A", Author: "Excelize", Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment."}}}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
comments, err := f.GetComments("Sheet1")
assert.NoError(t, err)
assert.Len(t, comments, 2)
comments, err = f.GetComments("Sheet2")
assert.NoError(t, err)
assert.Len(t, comments, 1)
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddComments.xlsx")))
f.Comments["xl/comments2.xml"] = nil
f.Pkg.Store("xl/comments2.xml", []byte(xml.Header+`<comments xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><authors><author>Excelize: </author></authors><commentList><comment ref="B7" authorId="0"><text><t>Excelize: </t></text></comment></commentList></comments>`))
comments, err = f.GetComments("Sheet1")
assert.NoError(t, err)
assert.Len(t, comments, 2)
comments, err = f.GetComments("Sheet2")
assert.NoError(t, err)
assert.Len(t, comments, 1)
comments, err = NewFile().GetComments("Sheet1")
assert.NoError(t, err)
assert.Len(t, comments, 0)
// Test add comments with invalid sheet name
assert.EqualError(t, f.AddComment("Sheet:1", Comment{Cell: "A1", Author: "Excelize", Text: "This is a comment."}), ErrSheetNameInvalid.Error())
// Test add comments with unsupported charset
f.Comments["xl/comments2.xml"] = nil
f.Pkg.Store("xl/comments2.xml", MacintoshCyrillicCharset)
_, err = f.GetComments("Sheet2")
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
// Test add comments with unsupported charset
f.Comments["xl/comments2.xml"] = nil
f.Pkg.Store("xl/comments2.xml", MacintoshCyrillicCharset)
assert.EqualError(t, f.AddComment("Sheet2", Comment{Cell: "A30", Text: "Comment"}), "XML syntax error on line 1: invalid UTF-8")
// Test add comments with unsupported charset style sheet
f.Styles = nil
f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset)
assert.EqualError(t, f.AddComment("Sheet2", Comment{Cell: "A30", Text: "Comment"}), "XML syntax error on line 1: invalid UTF-8")
// Test get comments on not exists worksheet
comments, err = f.GetComments("SheetN")
assert.Len(t, comments, 0)
assert.EqualError(t, err, "sheet SheetN does not exist")
}
func TestDeleteComment(t *testing.T) {
f, err := prepareTestBook1()
if !assert.NoError(t, err) {
t.FailNow()
}
assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "A40", Text: "Excelize: This is a comment1."}))
assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "A41", Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment2."}}}))
assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "C41", Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment3."}}}))
assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "C41", Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment3-1."}}}))
assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "C42", Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment4."}}}))
assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "C41", Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment2."}}}))
assert.NoError(t, f.DeleteComment("Sheet2", "A40"))
comments, err := f.GetComments("Sheet2")
assert.NoError(t, err)
assert.Len(t, comments, 5)
comments, err = NewFile().GetComments("Sheet1")
assert.NoError(t, err)
assert.Len(t, comments, 0)
// Test delete comment with invalid sheet name
assert.EqualError(t, f.DeleteComment("Sheet:1", "A1"), ErrSheetNameInvalid.Error())
// Test delete all comments in a worksheet
assert.NoError(t, f.DeleteComment("Sheet2", "A41"))
assert.NoError(t, f.DeleteComment("Sheet2", "C41"))
assert.NoError(t, f.DeleteComment("Sheet2", "C42"))
comments, err = f.GetComments("Sheet2")
assert.NoError(t, err)
assert.EqualValues(t, 0, len(comments))
// Test delete comment on not exists worksheet
assert.EqualError(t, f.DeleteComment("SheetN", "A1"), "sheet SheetN does not exist")
// Test delete comment with worksheet part
f.Pkg.Delete("xl/worksheets/sheet1.xml")
assert.NoError(t, f.DeleteComment("Sheet1", "A22"))
f.Comments["xl/comments2.xml"] = nil
f.Pkg.Store("xl/comments2.xml", MacintoshCyrillicCharset)
assert.EqualError(t, f.DeleteComment("Sheet2", "A41"), "XML syntax error on line 1: invalid UTF-8")
}
func TestDecodeVMLDrawingReader(t *testing.T) {
f := NewFile()
path := "xl/drawings/vmlDrawing1.xml"
f.Pkg.Store(path, MacintoshCyrillicCharset)
_, err := f.decodeVMLDrawingReader(path)
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
}
func TestCommentsReader(t *testing.T) {
f := NewFile()
// Test read comments with unsupported charset
path := "xl/comments1.xml"
f.Pkg.Store(path, MacintoshCyrillicCharset)
_, err := f.commentsReader(path)
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
}
func TestCountComments(t *testing.T) {
f := NewFile()
f.Comments["xl/comments1.xml"] = nil
assert.Equal(t, f.countComments(), 1)
}

View File

@ -1,4 +1,4 @@
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in // this source code is governed by a BSD-style license that can be found in
// the LICENSE file. // the LICENSE file.
// //
@ -7,7 +7,7 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming // Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of // API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.16 or later. // data. This library needs Go version 1.18 or later.
package excelize package excelize
@ -676,7 +676,7 @@ func (c *cfb) writeUint64(value int) {
c.writeBytes(buf) c.writeBytes(buf)
} }
// writeBytes write strings in the stream by a given value with an offset. // writeStrings write strings in the stream by a given value with an offset.
func (c *cfb) writeStrings(value string) { func (c *cfb) writeStrings(value string) {
encoder := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder() encoder := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder()
buffer, err := encoder.Bytes([]byte(value)) buffer, err := encoder.Bytes([]byte(value))

View File

@ -1,4 +1,4 @@
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in // this source code is governed by a BSD-style license that can be found in
// the LICENSE file. // the LICENSE file.
// //
@ -7,16 +7,19 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming // Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of // API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.16 or later. // data. This library needs Go version 1.18 or later.
package excelize package excelize
import ( import (
"bytes"
"encoding/binary"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"testing" "testing"
"github.com/richardlehane/mscfb"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -36,7 +39,7 @@ func TestEncrypt(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
raw[2050] = 3 raw[2050] = 3
_, err = Decrypt(raw, &Options{Password: "password"}) _, err = Decrypt(raw, &Options{Password: "password"})
assert.EqualError(t, err, ErrUnsupportedEncryptMechanism.Error()) assert.Equal(t, ErrUnsupportedEncryptMechanism, err)
// Test encrypt spreadsheet with invalid password // Test encrypt spreadsheet with invalid password
assert.EqualError(t, f.SaveAs(filepath.Join("test", "Encryption.xlsx"), Options{Password: strings.Repeat("*", MaxFieldLength+1)}), ErrPasswordLengthInvalid.Error()) assert.EqualError(t, f.SaveAs(filepath.Join("test", "Encryption.xlsx"), Options{Password: strings.Repeat("*", MaxFieldLength+1)}), ErrPasswordLengthInvalid.Error())
@ -51,6 +54,25 @@ func TestEncrypt(t *testing.T) {
// Test remove password by save workbook with options // Test remove password by save workbook with options
assert.NoError(t, f.Save(Options{Password: ""})) assert.NoError(t, f.Save(Options{Password: ""}))
assert.NoError(t, f.Close()) assert.NoError(t, f.Close())
doc, err := mscfb.New(bytes.NewReader(raw))
assert.NoError(t, err)
encryptionInfoBuf, encryptedPackageBuf := extractPart(doc)
binary.LittleEndian.PutUint64(encryptionInfoBuf[20:32], uint64(0))
_, err = standardDecrypt(encryptionInfoBuf, encryptedPackageBuf, &Options{Password: "password"})
assert.NoError(t, err)
_, err = decrypt(nil, nil, nil)
assert.EqualError(t, err, "crypto/aes: invalid key size 0")
_, err = agileDecrypt(encryptionInfoBuf, MacintoshCyrillicCharset, &Options{Password: "password"})
assert.EqualError(t, err, "XML syntax error on line 1: invalid character entity &0 (no semicolon)")
_, err = convertPasswdToKey("password", nil, Encryption{
KeyEncryptors: KeyEncryptors{KeyEncryptor: []KeyEncryptor{
{EncryptedKey: EncryptedKey{KeyData: KeyData{SaltValue: "=="}}},
}},
})
assert.EqualError(t, err, "illegal base64 data at input byte 0")
_, err = createIV([]byte{0}, Encryption{KeyData: KeyData{SaltValue: "=="}})
assert.EqualError(t, err, "illegal base64 data at input byte 0")
} }
func TestEncryptionMechanism(t *testing.T) { func TestEncryptionMechanism(t *testing.T) {

View File

@ -1,4 +1,4 @@
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in // this source code is governed by a BSD-style license that can be found in
// the LICENSE file. // the LICENSE file.
// //
@ -7,12 +7,13 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming // Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of // API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.16 or later. // data. This library needs Go version 1.18 or later.
package excelize package excelize
import ( import (
"fmt" "fmt"
"io"
"math" "math"
"strings" "strings"
"unicode/utf16" "unicode/utf16"
@ -23,15 +24,14 @@ type DataValidationType int
// Data validation types. // Data validation types.
const ( const (
_DataValidationType = iota _ DataValidationType = iota
typeNone // inline use DataValidationTypeNone
DataValidationTypeCustom DataValidationTypeCustom
DataValidationTypeDate DataValidationTypeDate
DataValidationTypeDecimal DataValidationTypeDecimal
typeList // inline use DataValidationTypeList
DataValidationTypeTextLength DataValidationTypeTextLength
DataValidationTypeTime DataValidationTypeTime
// DataValidationTypeWhole Integer
DataValidationTypeWhole DataValidationTypeWhole
) )
@ -58,7 +58,7 @@ type DataValidationOperator int
// Data validation operators. // Data validation operators.
const ( const (
_DataValidationOperator = iota _ DataValidationOperator = iota
DataValidationOperatorBetween DataValidationOperatorBetween
DataValidationOperatorEqual DataValidationOperatorEqual
DataValidationOperatorGreaterThan DataValidationOperatorGreaterThan
@ -69,13 +69,41 @@ const (
DataValidationOperatorNotEqual DataValidationOperatorNotEqual
) )
// formulaEscaper mimics the Excel escaping rules for data validation, var (
// which converts `"` to `""` instead of `&quot;`. // formulaEscaper mimics the Excel escaping rules for data validation,
var formulaEscaper = strings.NewReplacer( // which converts `"` to `""` instead of `&quot;`.
`&`, `&amp;`, formulaEscaper = strings.NewReplacer(
`<`, `&lt;`, `&`, `&amp;`,
`>`, `&gt;`, `<`, `&lt;`,
`"`, `""`, `>`, `&gt;`,
)
formulaUnescaper = strings.NewReplacer(
`&amp;`, `&`,
`&lt;`, `<`,
`&gt;`, `>`,
)
// dataValidationTypeMap defined supported data validation types.
dataValidationTypeMap = map[DataValidationType]string{
DataValidationTypeNone: "none",
DataValidationTypeCustom: "custom",
DataValidationTypeDate: "date",
DataValidationTypeDecimal: "decimal",
DataValidationTypeList: "list",
DataValidationTypeTextLength: "textLength",
DataValidationTypeTime: "time",
DataValidationTypeWhole: "whole",
}
// dataValidationOperatorMap defined supported data validation operators.
dataValidationOperatorMap = map[DataValidationOperator]string{
DataValidationOperatorBetween: "between",
DataValidationOperatorEqual: "equal",
DataValidationOperatorGreaterThan: "greaterThan",
DataValidationOperatorGreaterThanOrEqual: "greaterThanOrEqual",
DataValidationOperatorLessThan: "lessThan",
DataValidationOperatorLessThanOrEqual: "lessThanOrEqual",
DataValidationOperatorNotBetween: "notBetween",
DataValidationOperatorNotEqual: "notEqual",
}
) )
// NewDataValidation return data validation struct. // NewDataValidation return data validation struct.
@ -112,113 +140,92 @@ func (dv *DataValidation) SetInput(title, msg string) {
dv.Prompt = &msg dv.Prompt = &msg
} }
// SetDropList data validation list. // SetDropList data validation list. If you type the items into the data
// validation dialog box (a delimited list), the limit is 255 characters,
// including the separators. If your data validation list source formula is
// over the maximum length limit, please set the allowed values in the
// worksheet cells, and use the SetSqrefDropList function to set the reference
// for their cells.
func (dv *DataValidation) SetDropList(keys []string) error { func (dv *DataValidation) SetDropList(keys []string) error {
formula := strings.Join(keys, ",") formula := strings.Join(keys, ",")
if MaxFieldLength < len(utf16.Encode([]rune(formula))) { if MaxFieldLength < len(utf16.Encode([]rune(formula))) {
return ErrDataValidationFormulaLength return ErrDataValidationFormulaLength
} }
dv.Formula1 = fmt.Sprintf(`<formula1>"%s"</formula1>`, formulaEscaper.Replace(formula)) dv.Type = dataValidationTypeMap[DataValidationTypeList]
dv.Type = convDataValidationType(typeList) if strings.HasPrefix(formula, "=") {
dv.Formula1 = formulaEscaper.Replace(formula)
return nil
}
dv.Formula1 = fmt.Sprintf(`"%s"`, strings.NewReplacer(`"`, `""`).Replace(formulaEscaper.Replace(formula)))
return nil return nil
} }
// SetRange provides function to set data validation range in drop list, only // SetRange provides function to set data validation range in drop list, only
// accepts int, float64, or string data type formula argument. // accepts int, float64, string or []string data type formula argument.
func (dv *DataValidation) SetRange(f1, f2 interface{}, t DataValidationType, o DataValidationOperator) error { func (dv *DataValidation) SetRange(f1, f2 interface{}, t DataValidationType, o DataValidationOperator) error {
var formula1, formula2 string genFormula := func(val interface{}) (string, error) {
switch v := f1.(type) { var formula string
case int: switch v := val.(type) {
formula1 = fmt.Sprintf("<formula1>%d</formula1>", v) case int:
case float64: formula = fmt.Sprintf("%d", v)
if math.Abs(v) > math.MaxFloat32 { case float64:
return ErrDataValidationRange if math.Abs(v) > math.MaxFloat32 {
return formula, ErrDataValidationRange
}
formula = fmt.Sprintf("%.17g", v)
case string:
formula = v
default:
return formula, ErrParameterInvalid
} }
formula1 = fmt.Sprintf("<formula1>%.17g</formula1>", v) return formula, nil
case string:
formula1 = fmt.Sprintf("<formula1>%s</formula1>", v)
default:
return ErrParameterInvalid
} }
switch v := f2.(type) { formula1, err := genFormula(f1)
case int: if err != nil {
formula2 = fmt.Sprintf("<formula2>%d</formula2>", v) return err
case float64: }
if math.Abs(v) > math.MaxFloat32 { formula2, err := genFormula(f2)
return ErrDataValidationRange if err != nil {
} return err
formula2 = fmt.Sprintf("<formula2>%.17g</formula2>", v)
case string:
formula2 = fmt.Sprintf("<formula2>%s</formula2>", v)
default:
return ErrParameterInvalid
} }
dv.Formula1, dv.Formula2 = formula1, formula2 dv.Formula1, dv.Formula2 = formula1, formula2
dv.Type = convDataValidationType(t) dv.Type = dataValidationTypeMap[t]
dv.Operator = convDataValidationOperator(o) dv.Operator = dataValidationOperatorMap[o]
return nil return err
} }
// SetSqrefDropList provides set data validation on a range with source // SetSqrefDropList provides set data validation on a range with source
// reference range of the worksheet by given data validation object and // reference range of the worksheet by given data validation object and
// worksheet name. The data validation object can be created by // worksheet name. The data validation object can be created by
// NewDataValidation function. For example, set data validation on // NewDataValidation function. There are limits to the number of items that
// Sheet1!A7:B8 with validation criteria source Sheet1!E1:E3 settings, create // will show in a data validation drop down list: The list can show up to show
// in-cell dropdown by allowing list source: // 32768 items from a list on the worksheet. If you need more items than that,
// you could create a dependent drop down list, broken down by category. For
// example, set data validation on Sheet1!A7:B8 with validation criteria source
// Sheet1!E1:E3 settings, create in-cell dropdown by allowing list source:
// //
// dv := excelize.NewDataValidation(true) // dv := excelize.NewDataValidation(true)
// dv.Sqref = "A7:B8" // dv.Sqref = "A7:B8"
// dv.SetSqrefDropList("$E$1:$E$3") // dv.SetSqrefDropList("$E$1:$E$3")
// err := f.AddDataValidation("Sheet1", dv) // err := f.AddDataValidation("Sheet1", dv)
func (dv *DataValidation) SetSqrefDropList(sqref string) { func (dv *DataValidation) SetSqrefDropList(sqref string) {
dv.Formula1 = fmt.Sprintf("<formula1>%s</formula1>", sqref) dv.Formula1 = sqref
dv.Type = convDataValidationType(typeList) dv.Type = dataValidationTypeMap[DataValidationTypeList]
} }
// SetSqref provides function to set data validation range in drop list. // SetSqref provides function to set data validation range in drop list.
func (dv *DataValidation) SetSqref(sqref string) { func (dv *DataValidation) SetSqref(sqref string) {
if dv.Sqref == "" { if dv.Sqref == "" {
dv.Sqref = sqref dv.Sqref = sqref
} else { return
dv.Sqref = fmt.Sprintf("%s %s", dv.Sqref, sqref)
} }
} dv.Sqref = fmt.Sprintf("%s %s", dv.Sqref, sqref)
// convDataValidationType get excel data validation type.
func convDataValidationType(t DataValidationType) string {
typeMap := map[DataValidationType]string{
typeNone: "none",
DataValidationTypeCustom: "custom",
DataValidationTypeDate: "date",
DataValidationTypeDecimal: "decimal",
typeList: "list",
DataValidationTypeTextLength: "textLength",
DataValidationTypeTime: "time",
DataValidationTypeWhole: "whole",
}
return typeMap[t]
}
// convDataValidationOperator get excel data validation operator.
func convDataValidationOperator(o DataValidationOperator) string {
typeMap := map[DataValidationOperator]string{
DataValidationOperatorBetween: "between",
DataValidationOperatorEqual: "equal",
DataValidationOperatorGreaterThan: "greaterThan",
DataValidationOperatorGreaterThanOrEqual: "greaterThanOrEqual",
DataValidationOperatorLessThan: "lessThan",
DataValidationOperatorLessThanOrEqual: "lessThanOrEqual",
DataValidationOperatorNotBetween: "notBetween",
DataValidationOperatorNotEqual: "notEqual",
}
return typeMap[o]
} }
// AddDataValidation provides set data validation on a range of the worksheet // AddDataValidation provides set data validation on a range of the worksheet
// by given data validation object and worksheet name. The data validation // by given data validation object and worksheet name. This function is
// object can be created by NewDataValidation function. // concurrency safe. The data validation object can be created by
// NewDataValidation function.
// //
// Example 1, set data validation on Sheet1!A1:B2 with validation criteria // Example 1, set data validation on Sheet1!A1:B2 with validation criteria
// settings, show error alert after invalid data is entered with "Stop" style // settings, show error alert after invalid data is entered with "Stop" style
@ -251,10 +258,32 @@ func (f *File) AddDataValidation(sheet string, dv *DataValidation) error {
if err != nil { if err != nil {
return err return err
} }
ws.mu.Lock()
defer ws.mu.Unlock()
if nil == ws.DataValidations { if nil == ws.DataValidations {
ws.DataValidations = new(xlsxDataValidations) ws.DataValidations = new(xlsxDataValidations)
} }
ws.DataValidations.DataValidation = append(ws.DataValidations.DataValidation, dv) dataValidation := &xlsxDataValidation{
AllowBlank: dv.AllowBlank,
Error: dv.Error,
ErrorStyle: dv.ErrorStyle,
ErrorTitle: dv.ErrorTitle,
Operator: dv.Operator,
Prompt: dv.Prompt,
PromptTitle: dv.PromptTitle,
ShowDropDown: dv.ShowDropDown,
ShowErrorMessage: dv.ShowErrorMessage,
ShowInputMessage: dv.ShowInputMessage,
Sqref: dv.Sqref,
Type: dv.Type,
}
if dv.Formula1 != "" {
dataValidation.Formula1 = &xlsxInnerXML{Content: dv.Formula1}
}
if dv.Formula2 != "" {
dataValidation.Formula2 = &xlsxInnerXML{Content: dv.Formula2}
}
ws.DataValidations.DataValidation = append(ws.DataValidations.DataValidation, dataValidation)
ws.DataValidations.Count = len(ws.DataValidations.DataValidation) ws.DataValidations.Count = len(ws.DataValidations.DataValidation)
return err return err
} }
@ -265,20 +294,83 @@ func (f *File) GetDataValidations(sheet string) ([]*DataValidation, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
if ws.DataValidations == nil || len(ws.DataValidations.DataValidation) == 0 { var (
return nil, err dataValidations []*DataValidation
decodeExtLst = new(decodeExtLst)
decodeDataValidations *xlsxDataValidations
ext *xlsxExt
)
if ws.DataValidations != nil {
dataValidations = append(dataValidations, getDataValidations(ws.DataValidations)...)
} }
return ws.DataValidations.DataValidation, err if ws.ExtLst != nil {
if err = f.xmlNewDecoder(strings.NewReader("<extLst>" + ws.ExtLst.Ext + "</extLst>")).
Decode(decodeExtLst); err != nil && err != io.EOF {
return dataValidations, err
}
for _, ext = range decodeExtLst.Ext {
if ext.URI == ExtURIDataValidations {
decodeDataValidations = new(xlsxDataValidations)
_ = f.xmlNewDecoder(strings.NewReader(ext.Content)).Decode(decodeDataValidations)
dataValidations = append(dataValidations, getDataValidations(decodeDataValidations)...)
}
}
}
return dataValidations, err
}
// getDataValidations returns data validations list by given worksheet data
// validations.
func getDataValidations(dvs *xlsxDataValidations) []*DataValidation {
if dvs == nil {
return nil
}
var dataValidations []*DataValidation
for _, dv := range dvs.DataValidation {
if dv == nil {
continue
}
dataValidation := &DataValidation{
AllowBlank: dv.AllowBlank,
Error: dv.Error,
ErrorStyle: dv.ErrorStyle,
ErrorTitle: dv.ErrorTitle,
Operator: dv.Operator,
Prompt: dv.Prompt,
PromptTitle: dv.PromptTitle,
ShowDropDown: dv.ShowDropDown,
ShowErrorMessage: dv.ShowErrorMessage,
ShowInputMessage: dv.ShowInputMessage,
Sqref: dv.Sqref,
Type: dv.Type,
}
if dv.Formula1 != nil {
dataValidation.Formula1 = unescapeDataValidationFormula(dv.Formula1.Content)
}
if dv.Formula2 != nil {
dataValidation.Formula2 = unescapeDataValidationFormula(dv.Formula2.Content)
}
if dv.XMSqref != "" {
dataValidation.Sqref = dv.XMSqref
dataValidation.Formula1 = strings.TrimSuffix(strings.TrimPrefix(dataValidation.Formula1, "<xm:f>"), "</xm:f>")
dataValidation.Formula2 = strings.TrimSuffix(strings.TrimPrefix(dataValidation.Formula2, "<xm:f>"), "</xm:f>")
}
dataValidations = append(dataValidations, dataValidation)
}
return dataValidations
} }
// DeleteDataValidation delete data validation by given worksheet name and // DeleteDataValidation delete data validation by given worksheet name and
// reference sequence. All data validations in the worksheet will be deleted // reference sequence. This function is concurrency safe.
// All data validations in the worksheet will be deleted
// if not specify reference sequence parameter. // if not specify reference sequence parameter.
func (f *File) DeleteDataValidation(sheet string, sqref ...string) error { func (f *File) DeleteDataValidation(sheet string, sqref ...string) error {
ws, err := f.workSheetReader(sheet) ws, err := f.workSheetReader(sheet)
if err != nil { if err != nil {
return err return err
} }
ws.mu.Lock()
defer ws.mu.Unlock()
if ws.DataValidations == nil { if ws.DataValidations == nil {
return nil return nil
} }
@ -286,14 +378,14 @@ func (f *File) DeleteDataValidation(sheet string, sqref ...string) error {
ws.DataValidations = nil ws.DataValidations = nil
return nil return nil
} }
delCells, err := f.flatSqref(sqref[0]) delCells, err := flatSqref(sqref[0])
if err != nil { if err != nil {
return err return err
} }
dv := ws.DataValidations dv := ws.DataValidations
for i := 0; i < len(dv.DataValidation); i++ { for i := 0; i < len(dv.DataValidation); i++ {
var applySqref []string var applySqref []string
colCells, err := f.flatSqref(dv.DataValidation[i].Sqref) colCells, err := flatSqref(dv.DataValidation[i].Sqref)
if err != nil { if err != nil {
return err return err
} }
@ -306,7 +398,7 @@ func (f *File) DeleteDataValidation(sheet string, sqref ...string) error {
} }
} }
for _, col := range colCells { for _, col := range colCells {
applySqref = append(applySqref, f.squashSqref(col)...) applySqref = append(applySqref, squashSqref(col)...)
} }
dv.DataValidation[i].Sqref = strings.Join(applySqref, " ") dv.DataValidation[i].Sqref = strings.Join(applySqref, " ")
if len(applySqref) == 0 { if len(applySqref) == 0 {
@ -322,30 +414,43 @@ func (f *File) DeleteDataValidation(sheet string, sqref ...string) error {
} }
// squashSqref generates cell reference sequence by given cells coordinates list. // squashSqref generates cell reference sequence by given cells coordinates list.
func (f *File) squashSqref(cells [][]int) []string { func squashSqref(cells [][]int) []string {
if len(cells) == 1 { if len(cells) == 1 {
cell, _ := CoordinatesToCellName(cells[0][0], cells[0][1]) cell, _ := CoordinatesToCellName(cells[0][0], cells[0][1])
return []string{cell} return []string{cell}
} else if len(cells) == 0 { } else if len(cells) == 0 {
return []string{} return []string{}
} }
var res []string var refs []string
l, r := 0, 0 l, r := 0, 0
for i := 1; i < len(cells); i++ { for i := 1; i < len(cells); i++ {
if cells[i][0] == cells[r][0] && cells[i][1]-cells[r][1] > 1 { if cells[i][0] == cells[r][0] && cells[i][1]-cells[r][1] > 1 {
curr, _ := f.coordinatesToRangeRef(append(cells[l], cells[r]...)) ref, _ := coordinatesToRangeRef(append(cells[l], cells[r]...))
if l == r { if l == r {
curr, _ = CoordinatesToCellName(cells[l][0], cells[l][1]) ref, _ = CoordinatesToCellName(cells[l][0], cells[l][1])
} }
res = append(res, curr) refs = append(refs, ref)
l, r = i, i l, r = i, i
} else { } else {
r++ r++
} }
} }
curr, _ := f.coordinatesToRangeRef(append(cells[l], cells[r]...)) ref, _ := coordinatesToRangeRef(append(cells[l], cells[r]...))
if l == r { if l == r {
curr, _ = CoordinatesToCellName(cells[l][0], cells[l][1]) ref, _ = CoordinatesToCellName(cells[l][0], cells[l][1])
} }
return append(res, curr) return append(refs, ref)
}
// isFormulaDataValidation returns whether the data validation rule is a formula.
func (dv *xlsxInnerXML) isFormula() bool {
return dv != nil && !(strings.HasPrefix(dv.Content, "&quot;") && strings.HasSuffix(dv.Content, "&quot;"))
}
// unescapeDataValidationFormula returns unescaped data validation formula.
func unescapeDataValidationFormula(val string) string {
if strings.HasPrefix(val, "\"") { // Text detection
return strings.NewReplacer(`""`, `"`).Replace(formulaUnescaper.Replace(val))
}
return formulaUnescaper.Replace(val)
} }

View File

@ -1,4 +1,4 @@
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in // this source code is governed by a BSD-style license that can be found in
// the LICENSE file. // the LICENSE file.
// //
@ -7,11 +7,12 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming // Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of // API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.16 or later. // data. This library needs Go version 1.18 or later.
package excelize package excelize
import ( import (
"fmt"
"math" "math"
"path/filepath" "path/filepath"
"strings" "strings"
@ -71,6 +72,7 @@ func TestDataValidation(t *testing.T) {
dv.Sqref = "A5:B6" dv.Sqref = "A5:B6"
for _, listValid := range [][]string{ for _, listValid := range [][]string{
{"1", "2", "3"}, {"1", "2", "3"},
{"=A1"},
{strings.Repeat("&", MaxFieldLength)}, {strings.Repeat("&", MaxFieldLength)},
{strings.Repeat("\u4E00", MaxFieldLength)}, {strings.Repeat("\u4E00", MaxFieldLength)},
{strings.Repeat("\U0001F600", 100), strings.Repeat("\u4E01", 50), "<&>"}, {strings.Repeat("\U0001F600", 100), strings.Repeat("\u4E01", 50), "<&>"},
@ -82,7 +84,7 @@ func TestDataValidation(t *testing.T) {
assert.NotEqual(t, "", dv.Formula1, assert.NotEqual(t, "", dv.Formula1,
"Formula1 should not be empty for valid input %v", listValid) "Formula1 should not be empty for valid input %v", listValid)
} }
assert.Equal(t, `<formula1>"A&lt;,B&gt;,C"",D ,E',F"</formula1>`, dv.Formula1) assert.Equal(t, `"A&lt;,B&gt;,C"",D ,E',F"`, dv.Formula1)
assert.NoError(t, f.AddDataValidation("Sheet1", dv)) assert.NoError(t, f.AddDataValidation("Sheet1", dv))
dataValidations, err = f.GetDataValidations("Sheet1") dataValidations, err = f.GetDataValidations("Sheet1")
@ -103,6 +105,31 @@ func TestDataValidation(t *testing.T) {
dataValidations, err = f.GetDataValidations("Sheet1") dataValidations, err = f.GetDataValidations("Sheet1")
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, []*DataValidation(nil), dataValidations) assert.Equal(t, []*DataValidation(nil), dataValidations)
// Test get data validations which storage in the extension lists
f = NewFile()
ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
assert.True(t, ok)
ws.(*xlsxWorksheet).ExtLst = &xlsxExtLst{Ext: fmt.Sprintf(`<ext uri="%s" xmlns:x14="%s"><x14:dataValidations><x14:dataValidation type="list" allowBlank="1"><x14:formula1><xm:f>Sheet1!$B$1:$B$5</xm:f></x14:formula1><xm:sqref>A7:B8</xm:sqref></x14:dataValidation></x14:dataValidations></ext>`, ExtURIDataValidations, NameSpaceSpreadSheetX14.Value)}
dataValidations, err = f.GetDataValidations("Sheet1")
assert.NoError(t, err)
assert.Equal(t, []*DataValidation{
{
AllowBlank: true,
Type: "list",
Formula1: "Sheet1!$B$1:$B$5",
Sqref: "A7:B8",
},
}, dataValidations)
// Test get data validations with invalid extension list characters
ws.(*xlsxWorksheet).ExtLst = &xlsxExtLst{Ext: fmt.Sprintf(`<ext uri="%s" xmlns:x14="%s"><x14:dataValidations></x14:dataValidation></x14:dataValidations></ext>`, ExtURIDataValidations, NameSpaceSpreadSheetX14.Value)}
_, err = f.GetDataValidations("Sheet1")
assert.EqualError(t, err, "XML syntax error on line 1: element <dataValidations> closed by </dataValidation>")
// Test get validations without validations
assert.Nil(t, getDataValidations(nil))
assert.Nil(t, getDataValidations(&xlsxDataValidations{DataValidation: []*xlsxDataValidation{nil}}))
} }
func TestDataValidationError(t *testing.T) { func TestDataValidationError(t *testing.T) {

36
date.go
View File

@ -1,4 +1,4 @@
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in // this source code is governed by a BSD-style license that can be found in
// the LICENSE file. // the LICENSE file.
// //
@ -7,7 +7,7 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming // Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of // API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.16 or later. // data. This library needs Go version 1.18 or later.
package excelize package excelize
@ -114,7 +114,7 @@ func julianDateToGregorianTime(part1, part2 float64) time.Time {
// "Communications of the ACM" in 1968 (published in CACM, volume 11, number // "Communications of the ACM" in 1968 (published in CACM, volume 11, number
// 10, October 1968, p.657). None of those programmers seems to have found it // 10, October 1968, p.657). None of those programmers seems to have found it
// necessary to explain the constants or variable names set out by Henry F. // necessary to explain the constants or variable names set out by Henry F.
// Fliegel and Thomas C. Van Flandern. Maybe one day I'll buy that jounal and // Fliegel and Thomas C. Van Flandern. Maybe one day I'll buy that journal and
// expand an explanation here - that day is not today. // expand an explanation here - that day is not today.
func doTheFliegelAndVanFlandernAlgorithm(jd int) (day, month, year int) { func doTheFliegelAndVanFlandernAlgorithm(jd int) (day, month, year int) {
l := jd + 68569 l := jd + 68569
@ -163,7 +163,7 @@ func timeFromExcelTime(excelTime float64, date1904 bool) time.Time {
return date.Truncate(time.Second) return date.Truncate(time.Second)
} }
// ExcelDateToTime converts a float-based excel date representation to a time.Time. // ExcelDateToTime converts a float-based Excel date representation to a time.Time.
func ExcelDateToTime(excelDate float64, use1904Format bool) (time.Time, error) { func ExcelDateToTime(excelDate float64, use1904Format bool) (time.Time, error) {
if excelDate < 0 { if excelDate < 0 {
return time.Time{}, newInvalidExcelDateError(excelDate) return time.Time{}, newInvalidExcelDateError(excelDate)
@ -214,3 +214,31 @@ func formatYear(y int) int {
} }
return y return y
} }
// getDurationNumFmt returns most simplify numbers format code for time
// duration type cell value by given worksheet name, cell reference and number.
func getDurationNumFmt(d time.Duration) int {
if d >= time.Hour*24 {
return 46
}
// Whole minutes
if d.Minutes() == float64(int(d.Minutes())) {
return 20
}
return 21
}
// getTimeNumFmt returns most simplify numbers format code for time type cell
// value by given worksheet name, cell reference and number.
func getTimeNumFmt(t time.Time) int {
nextMonth := t.AddDate(0, 1, 0)
// Whole months
if t.Day() == 1 && nextMonth.Day() == 1 {
return 17
}
// Whole days
if t.Hour() == 0 && t.Minute() == 0 && t.Second() == 0 && t.Nanosecond() == 0 {
return 14
}
return 22
}

View File

@ -68,22 +68,22 @@ func TestTimeFromExcelTime(t *testing.T) {
}) })
} }
for hour := 0; hour < 24; hour++ { for hour := 0; hour < 24; hour++ {
for min := 0; min < 60; min++ { for minVal := 0; minVal < 60; minVal++ {
for sec := 0; sec < 60; sec++ { for sec := 0; sec < 60; sec++ {
date := time.Date(2021, time.December, 30, hour, min, sec, 0, time.UTC) date := time.Date(2021, time.December, 30, hour, minVal, sec, 0, time.UTC)
// Test use 1900 date system // Test use 1900 date system
excel1900Time, err := timeToExcelTime(date, false) excel1900Time, err := timeToExcelTime(date, false)
assert.NoError(t, err) assert.NoError(t, err)
date1900Out := timeFromExcelTime(excel1900Time, false) date1900Out := timeFromExcelTime(excel1900Time, false)
assert.EqualValues(t, hour, date1900Out.Hour()) assert.EqualValues(t, hour, date1900Out.Hour())
assert.EqualValues(t, min, date1900Out.Minute()) assert.EqualValues(t, minVal, date1900Out.Minute())
assert.EqualValues(t, sec, date1900Out.Second()) assert.EqualValues(t, sec, date1900Out.Second())
// Test use 1904 date system // Test use 1904 date system
excel1904Time, err := timeToExcelTime(date, true) excel1904Time, err := timeToExcelTime(date, true)
assert.NoError(t, err) assert.NoError(t, err)
date1904Out := timeFromExcelTime(excel1904Time, true) date1904Out := timeFromExcelTime(excel1904Time, true)
assert.EqualValues(t, hour, date1904Out.Hour()) assert.EqualValues(t, hour, date1904Out.Hour())
assert.EqualValues(t, min, date1904Out.Minute()) assert.EqualValues(t, minVal, date1904Out.Minute())
assert.EqualValues(t, sec, date1904Out.Second()) assert.EqualValues(t, sec, date1904Out.Second())
} }
} }

View File

@ -1,4 +1,4 @@
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in // this source code is governed by a BSD-style license that can be found in
// the LICENSE file. // the LICENSE file.
// //
@ -7,7 +7,7 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming // Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of // API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.16 or later. // data. This library needs Go version 1.18 or later.
package excelize package excelize

View File

@ -1,4 +1,4 @@
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in // this source code is governed by a BSD-style license that can be found in
// the LICENSE file. // the LICENSE file.
// //
@ -7,7 +7,7 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming // Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of // API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.16 or later. // data. This library needs Go version 1.18 or later.
package excelize package excelize

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in // this source code is governed by a BSD-style license that can be found in
// the LICENSE file. // the LICENSE file.
// //
@ -7,7 +7,7 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming // Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of // API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.16 or later. // data. This library needs Go version 1.18 or later.
package excelize package excelize
@ -38,3 +38,12 @@ func TestDrawingParser(t *testing.T) {
_, _, err = f.drawingParser("wsDr") _, _, err = f.drawingParser("wsDr")
assert.NoError(t, err) assert.NoError(t, err)
} }
func TestDeleteDrawingRels(t *testing.T) {
f := NewFile()
// Test delete drawing relationships with unsupported charset
rels := "xl/drawings/_rels/drawing1.xml.rels"
f.Relationships.Delete(rels)
f.Pkg.Store(rels, MacintoshCyrillicCharset)
f.deleteDrawingRels(rels, "")
}

483
errors.go
View File

@ -1,4 +1,4 @@
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in // this source code is governed by a BSD-style license that can be found in
// the LICENSE file. // the LICENSE file.
// //
@ -7,7 +7,7 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming // Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of // API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.16 or later. // data. This library needs Go version 1.18 or later.
package excelize package excelize
@ -16,16 +16,210 @@ import (
"fmt" "fmt"
) )
// newInvalidColumnNameError defined the error message on receiving the var (
// invalid column name. // ErrAddVBAProject defined the error message on add the VBA project in
func newInvalidColumnNameError(col string) error { // the workbook.
return fmt.Errorf("invalid column name %q", col) ErrAddVBAProject = errors.New("unsupported VBA project")
// ErrAttrValBool defined the error message on marshal and unmarshal
// boolean type XML attribute.
ErrAttrValBool = errors.New("unexpected child of attrValBool")
// ErrCellCharsLength defined the error message for receiving a cell
// characters length that exceeds the limit.
ErrCellCharsLength = fmt.Errorf("cell value must be 0-%d characters", TotalCellChars)
// ErrCellStyles defined the error message on cell styles exceeds the limit.
ErrCellStyles = fmt.Errorf("the cell styles exceeds the %d limit", MaxCellStyles)
// ErrColumnNumber defined the error message on receive an invalid column
// number.
ErrColumnNumber = fmt.Errorf("the column number must be greater than or equal to %d and less than or equal to %d", MinColumns, MaxColumns)
// ErrColumnWidth defined the error message on receive an invalid column
// width.
ErrColumnWidth = fmt.Errorf("the width of the column must be less than or equal to %d characters", MaxColumnWidth)
// ErrCoordinates defined the error message on invalid coordinates tuples
// length.
ErrCoordinates = errors.New("coordinates length must be 4")
// ErrCustomNumFmt defined the error message on receive the empty custom number format.
ErrCustomNumFmt = errors.New("custom number format can not be empty")
// ErrDataValidationFormulaLength defined the error message for receiving a
// data validation formula length that exceeds the limit.
ErrDataValidationFormulaLength = fmt.Errorf("data validation must be 0-%d characters", MaxFieldLength)
// ErrDataValidationRange defined the error message on set decimal range
// exceeds limit.
ErrDataValidationRange = errors.New("data validation range exceeds limit")
// ErrDefinedNameDuplicate defined the error message on the same name
// already exists on the scope.
ErrDefinedNameDuplicate = errors.New("the same name already exists on the scope")
// ErrDefinedNameScope defined the error message on not found defined name
// in the given scope.
ErrDefinedNameScope = errors.New("no defined name on the scope")
// ErrExistsSheet defined the error message on given sheet already exists.
ErrExistsSheet = errors.New("the same name sheet already exists")
// ErrExistsTableName defined the error message on given table already exists.
ErrExistsTableName = errors.New("the same name table already exists")
// ErrFontLength defined the error message on the length of the font
// family name overflow.
ErrFontLength = fmt.Errorf("the length of the font family name must be less than or equal to %d", MaxFontFamilyLength)
// ErrFontSize defined the error message on the size of the font is invalid.
ErrFontSize = fmt.Errorf("font size must be between %d and %d points", MinFontSize, MaxFontSize)
// ErrFormControlValue defined the error message for receiving a scroll
// value exceeds limit.
ErrFormControlValue = fmt.Errorf("scroll value must be between 0 and %d", MaxFormControlValue)
// ErrGroupSheets defined the error message on group sheets.
ErrGroupSheets = errors.New("group worksheet must contain an active worksheet")
// ErrImgExt defined the error message on receive an unsupported image
// extension.
ErrImgExt = errors.New("unsupported image extension")
// ErrInvalidFormula defined the error message on receive an invalid
// formula.
ErrInvalidFormula = errors.New("formula not valid")
// ErrMaxFilePathLength defined the error message on receive the file path
// length overflow.
ErrMaxFilePathLength = fmt.Errorf("file path length exceeds maximum limit %d characters", MaxFilePathLength)
// ErrMaxRowHeight defined the error message on receive an invalid row
// height.
ErrMaxRowHeight = fmt.Errorf("the height of the row must be less than or equal to %d points", MaxRowHeight)
// ErrMaxRows defined the error message on receive a row number exceeds maximum limit.
ErrMaxRows = errors.New("row number exceeds maximum limit")
// ErrNameLength defined the error message on receiving the defined name or
// table name length exceeds the limit.
ErrNameLength = fmt.Errorf("the name length exceeds the %d characters limit", MaxFieldLength)
// ErrOptionsUnzipSizeLimit defined the error message for receiving
// invalid UnzipSizeLimit and UnzipXMLSizeLimit.
ErrOptionsUnzipSizeLimit = errors.New("the value of UnzipSizeLimit should be greater than or equal to UnzipXMLSizeLimit")
// ErrOutlineLevel defined the error message on receive an invalid outline
// level number.
ErrOutlineLevel = errors.New("invalid outline level")
// ErrPageSetupAdjustTo defined the error message for receiving a page setup
// adjust to value exceeds limit.
ErrPageSetupAdjustTo = errors.New("adjust to value must be between 10 and 400")
// ErrParameterInvalid defined the error message on receive the invalid
// parameter.
ErrParameterInvalid = errors.New("parameter is invalid")
// ErrParameterRequired defined the error message on receive the empty
// parameter.
ErrParameterRequired = errors.New("parameter is required")
// ErrPasswordLengthInvalid defined the error message on invalid password
// length.
ErrPasswordLengthInvalid = errors.New("password length invalid")
// ErrPivotTableClassicLayout defined the error message on enable
// ClassicLayout and CompactData in the same time.
ErrPivotTableClassicLayout = errors.New("cannot enable ClassicLayout and CompactData in the same time")
// ErrSave defined the error message for saving file.
ErrSave = errors.New("no path defined for file, consider File.WriteTo or File.Write")
// ErrSheetIdx defined the error message on receive the invalid worksheet
// index.
ErrSheetIdx = errors.New("invalid worksheet index")
// ErrSheetNameBlank defined the error message on receive the blank sheet
// name.
ErrSheetNameBlank = errors.New("the sheet name can not be blank")
// ErrSheetNameInvalid defined the error message on receive the sheet name
// contains invalid characters.
ErrSheetNameInvalid = errors.New("the sheet can not contain any of the characters :\\/?*[or]")
// ErrSheetNameLength defined the error message on receiving the sheet
// name length exceeds the limit.
ErrSheetNameLength = fmt.Errorf("the sheet name length exceeds the %d characters limit", MaxSheetNameLength)
// ErrSheetNameSingleQuote defined the error message on the first or last
// character of the sheet name was a single quote.
ErrSheetNameSingleQuote = errors.New("the first or last character of the sheet name can not be a single quote")
// ErrSparkline defined the error message on receive the invalid sparkline
// parameters.
ErrSparkline = errors.New("must have the same number of 'Location' and 'Range' parameters")
// ErrSparklineLocation defined the error message on missing Location
// parameters
ErrSparklineLocation = errors.New("parameter 'Location' is required")
// ErrSparklineRange defined the error message on missing sparkline Range
// parameters
ErrSparklineRange = errors.New("parameter 'Range' is required")
// ErrSparklineStyle defined the error message on receive the invalid
// sparkline Style parameters.
ErrSparklineStyle = errors.New("parameter 'Style' must between 0-35")
// ErrSparklineType defined the error message on receive the invalid
// sparkline Type parameters.
ErrSparklineType = errors.New("parameter 'Type' must be 'line', 'column' or 'win_loss'")
// ErrStreamSetColWidth defined the error message on set column width in
// stream writing mode.
ErrStreamSetColWidth = errors.New("must call the SetColWidth function before the SetRow function")
// ErrStreamSetPanes defined the error message on set panes in stream
// writing mode.
ErrStreamSetPanes = errors.New("must call the SetPanes function before the SetRow function")
// ErrTotalSheetHyperlinks defined the error message on hyperlinks count
// overflow.
ErrTotalSheetHyperlinks = errors.New("over maximum limit hyperlinks in a worksheet")
// ErrUnknownEncryptMechanism defined the error message on unsupported
// encryption mechanism.
ErrUnknownEncryptMechanism = errors.New("unknown encryption mechanism")
// ErrUnprotectSheet defined the error message on worksheet has set no
// protection.
ErrUnprotectSheet = errors.New("worksheet has set no protect")
// ErrUnprotectSheetPassword defined the error message on remove sheet
// protection with password verification failed.
ErrUnprotectSheetPassword = errors.New("worksheet protect password not match")
// ErrUnprotectWorkbook defined the error message on workbook has set no
// protection.
ErrUnprotectWorkbook = errors.New("workbook has set no protect")
// ErrUnprotectWorkbookPassword defined the error message on remove workbook
// protection with password verification failed.
ErrUnprotectWorkbookPassword = errors.New("workbook protect password not match")
// ErrUnsupportedEncryptMechanism defined the error message on unsupported
// encryption mechanism.
ErrUnsupportedEncryptMechanism = errors.New("unsupported encryption mechanism")
// ErrUnsupportedHashAlgorithm defined the error message on unsupported
// hash algorithm.
ErrUnsupportedHashAlgorithm = errors.New("unsupported hash algorithm")
// ErrUnsupportedNumberFormat defined the error message on unsupported number format
// expression.
ErrUnsupportedNumberFormat = errors.New("unsupported number format token")
// ErrWorkbookFileFormat defined the error message on receive an
// unsupported workbook file format.
ErrWorkbookFileFormat = errors.New("unsupported workbook file format")
// ErrWorkbookPassword defined the error message on receiving the incorrect
// workbook password.
ErrWorkbookPassword = errors.New("the supplied open workbook password is not correct")
)
// ErrSheetNotExist defined an error of sheet that does not exist.
type ErrSheetNotExist struct {
SheetName string
} }
// newInvalidRowNumberError defined the error message on receiving the invalid // Error returns the error message on receiving the non existing sheet name.
// row number. func (err ErrSheetNotExist) Error() string {
func newInvalidRowNumberError(row int) error { return fmt.Sprintf("sheet %s does not exist", err.SheetName)
return fmt.Errorf("invalid row number %d", row) }
// newCellNameToCoordinatesError defined the error message on converts
// alphanumeric cell name to coordinates.
func newCellNameToCoordinatesError(cell string, err error) error {
return fmt.Errorf("cannot convert cell %q to coordinates: %v", cell, err)
}
// newCoordinatesToCellNameError defined the error message on converts [X, Y]
// coordinates to alpha-numeric cell name.
func newCoordinatesToCellNameError(col, row int) error {
return fmt.Errorf("invalid cell reference [%d, %d]", col, row)
}
// newFieldLengthError defined the error message on receiving the field length
// overflow.
func newFieldLengthError(name string) error {
return fmt.Errorf("field %s must be less than or equal to 255 characters", name)
}
// newInvalidAutoFilterColumnError defined the error message on receiving the
// incorrect index of column.
func newInvalidAutoFilterColumnError(col string) error {
return fmt.Errorf("incorrect index of column %q", col)
}
// newInvalidAutoFilterExpError defined the error message on receiving the
// incorrect number of tokens in criteria expression.
func newInvalidAutoFilterExpError(exp string) error {
return fmt.Errorf("incorrect number of tokens in criteria %q", exp)
}
// newInvalidAutoFilterOperatorError defined the error message on receiving the
// incorrect expression operator.
func newInvalidAutoFilterOperatorError(op, exp string) error {
return fmt.Errorf("the operator %q in expression %q is not valid in relation to Blanks/NonBlanks", op, exp)
} }
// newInvalidCellNameError defined the error message on receiving the invalid // newInvalidCellNameError defined the error message on receiving the invalid
@ -34,16 +228,94 @@ func newInvalidCellNameError(cell string) error {
return fmt.Errorf("invalid cell name %q", cell) return fmt.Errorf("invalid cell name %q", cell)
} }
// newInvalidColumnNameError defined the error message on receiving the
// invalid column name.
func newInvalidColumnNameError(col string) error {
return fmt.Errorf("invalid column name %q", col)
}
// newInvalidExcelDateError defined the error message on receiving the data // newInvalidExcelDateError defined the error message on receiving the data
// with negative values. // with negative values.
func newInvalidExcelDateError(dateValue float64) error { func newInvalidExcelDateError(dateValue float64) error {
return fmt.Errorf("invalid date value %f, negative values are not supported", dateValue) return fmt.Errorf("invalid date value %f, negative values are not supported", dateValue)
} }
// newInvalidTableNameError defined the error message on receiving the invalid // newInvalidLinkTypeError defined the error message on receiving the invalid
// hyper link type.
func newInvalidLinkTypeError(linkType string) error {
return fmt.Errorf("invalid link type %q", linkType)
}
// newInvalidNameError defined the error message on receiving the invalid
// defined name or table name.
func newInvalidNameError(name string) error {
return fmt.Errorf("invalid name %q, the name should be starts with a letter or underscore, can not include a space or character, and can not conflict with an existing name in the workbook", name)
}
// newInvalidPageLayoutValueError defined the error message on receiving the invalid
// page layout options value.
func newInvalidPageLayoutValueError(name, value, msg string) error {
return fmt.Errorf("invalid %s value %q, acceptable value should be one of %s", name, value, msg)
}
// newInvalidRowNumberError defined the error message on receiving the invalid
// row number.
func newInvalidRowNumberError(row int) error {
return fmt.Errorf("invalid row number %d", row)
}
// newInvalidSlicerNameError defined the error message on receiving the invalid
// slicer name.
func newInvalidSlicerNameError(name string) error {
return fmt.Errorf("invalid slicer name %q", name)
}
// newInvalidStyleID defined the error message on receiving the invalid style
// ID.
func newInvalidStyleID(styleID int) error {
return fmt.Errorf("invalid style ID %d", styleID)
}
// newNoExistSlicerError defined the error message on receiving the non existing
// slicer name.
func newNoExistSlicerError(name string) error {
return fmt.Errorf("slicer %s does not exist", name)
}
// newNoExistTableError defined the error message on receiving the non existing
// table name. // table name.
func newInvalidTableNameError(name string) error { func newNoExistTableError(name string) error {
return fmt.Errorf("invalid table name %q", name) return fmt.Errorf("table %s does not exist", name)
}
// newNotWorksheetError defined the error message on receiving a sheet which
// not a worksheet.
func newNotWorksheetError(name string) error {
return fmt.Errorf("sheet %s is not a worksheet", name)
}
// newPivotTableDataRangeError defined the error message on receiving the
// invalid pivot table data range.
func newPivotTableDataRangeError(msg string) error {
return fmt.Errorf("parameter 'DataRange' parsing error: %s", msg)
}
// newPivotTableRangeError defined the error message on receiving the invalid
// pivot table range.
func newPivotTableRangeError(msg string) error {
return fmt.Errorf("parameter 'PivotTableRange' parsing error: %s", msg)
}
// newStreamSetRowError defined the error message on the stream writer
// receiving the non-ascending row number.
func newStreamSetRowError(row int) error {
return fmt.Errorf("row %d has already been written", row)
}
// newUnknownFilterTokenError defined the error message on receiving a unknown
// filter operator token.
func newUnknownFilterTokenError(token string) error {
return fmt.Errorf("unknown operator: %s", token)
} }
// newUnsupportedChartType defined the error message on receiving the chart // newUnsupportedChartType defined the error message on receiving the chart
@ -58,193 +330,8 @@ func newUnzipSizeLimitError(unzipSizeLimit int64) error {
return fmt.Errorf("unzip size exceeds the %d bytes limit", unzipSizeLimit) return fmt.Errorf("unzip size exceeds the %d bytes limit", unzipSizeLimit)
} }
// newInvalidStyleID defined the error message on receiving the invalid style
// ID.
func newInvalidStyleID(styleID int) error {
return fmt.Errorf("invalid style ID %d", styleID)
}
// newFieldLengthError defined the error message on receiving the field length
// overflow.
func newFieldLengthError(name string) error {
return fmt.Errorf("field %s must be less than or equal to 255 characters", name)
}
// newCellNameToCoordinatesError defined the error message on converts
// alphanumeric cell name to coordinates.
func newCellNameToCoordinatesError(cell string, err error) error {
return fmt.Errorf("cannot convert cell %q to coordinates: %v", cell, err)
}
// newNoExistSheetError defined the error message on receiving the non existing
// sheet name.
func newNoExistSheetError(name string) error {
return fmt.Errorf("sheet %s does not exist", name)
}
// newNotWorksheetError defined the error message on receiving a sheet which
// not a worksheet.
func newNotWorksheetError(name string) error {
return fmt.Errorf("sheet %s is not a worksheet", name)
}
// newStreamSetRowError defined the error message on the stream writer
// receiving the non-ascending row number.
func newStreamSetRowError(row int) error {
return fmt.Errorf("row %d has already been written", row)
}
// newViewIdxError defined the error message on receiving a invalid sheet view // newViewIdxError defined the error message on receiving a invalid sheet view
// index. // index.
func newViewIdxError(viewIndex int) error { func newViewIdxError(viewIndex int) error {
return fmt.Errorf("view index %d out of range", viewIndex) return fmt.Errorf("view index %d out of range", viewIndex)
} }
var (
// ErrStreamSetColWidth defined the error message on set column width in
// stream writing mode.
ErrStreamSetColWidth = errors.New("must call the SetColWidth function before the SetRow function")
// ErrStreamSetPanes defined the error message on set panes in stream
// writing mode.
ErrStreamSetPanes = errors.New("must call the SetPanes function before the SetRow function")
// ErrColumnNumber defined the error message on receive an invalid column
// number.
ErrColumnNumber = fmt.Errorf(`the column number must be greater than or equal to %d and less than or equal to %d`, MinColumns, MaxColumns)
// ErrColumnWidth defined the error message on receive an invalid column
// width.
ErrColumnWidth = fmt.Errorf("the width of the column must be less than or equal to %d characters", MaxColumnWidth)
// ErrOutlineLevel defined the error message on receive an invalid outline
// level number.
ErrOutlineLevel = errors.New("invalid outline level")
// ErrCoordinates defined the error message on invalid coordinates tuples
// length.
ErrCoordinates = errors.New("coordinates length must be 4")
// ErrExistsSheet defined the error message on given sheet already exists.
ErrExistsSheet = errors.New("the same name sheet already exists")
// ErrTotalSheetHyperlinks defined the error message on hyperlinks count
// overflow.
ErrTotalSheetHyperlinks = errors.New("over maximum limit hyperlinks in a worksheet")
// ErrInvalidFormula defined the error message on receive an invalid
// formula.
ErrInvalidFormula = errors.New("formula not valid")
// ErrAddVBAProject defined the error message on add the VBA project in
// the workbook.
ErrAddVBAProject = errors.New("unsupported VBA project")
// ErrMaxRows defined the error message on receive a row number exceeds maximum limit.
ErrMaxRows = errors.New("row number exceeds maximum limit")
// ErrMaxRowHeight defined the error message on receive an invalid row
// height.
ErrMaxRowHeight = fmt.Errorf("the height of the row must be less than or equal to %d points", MaxRowHeight)
// ErrImgExt defined the error message on receive an unsupported image
// extension.
ErrImgExt = errors.New("unsupported image extension")
// ErrWorkbookFileFormat defined the error message on receive an
// unsupported workbook file format.
ErrWorkbookFileFormat = errors.New("unsupported workbook file format")
// ErrMaxFilePathLength defined the error message on receive the file path
// length overflow.
ErrMaxFilePathLength = errors.New("file path length exceeds maximum limit")
// ErrUnknownEncryptMechanism defined the error message on unsupported
// encryption mechanism.
ErrUnknownEncryptMechanism = errors.New("unknown encryption mechanism")
// ErrUnsupportedEncryptMechanism defined the error message on unsupported
// encryption mechanism.
ErrUnsupportedEncryptMechanism = errors.New("unsupported encryption mechanism")
// ErrUnsupportedHashAlgorithm defined the error message on unsupported
// hash algorithm.
ErrUnsupportedHashAlgorithm = errors.New("unsupported hash algorithm")
// ErrUnsupportedNumberFormat defined the error message on unsupported number format
// expression.
ErrUnsupportedNumberFormat = errors.New("unsupported number format token")
// ErrPasswordLengthInvalid defined the error message on invalid password
// length.
ErrPasswordLengthInvalid = errors.New("password length invalid")
// ErrParameterRequired defined the error message on receive the empty
// parameter.
ErrParameterRequired = errors.New("parameter is required")
// ErrParameterInvalid defined the error message on receive the invalid
// parameter.
ErrParameterInvalid = errors.New("parameter is invalid")
// ErrDefinedNameScope defined the error message on not found defined name
// in the given scope.
ErrDefinedNameScope = errors.New("no defined name on the scope")
// ErrDefinedNameDuplicate defined the error message on the same name
// already exists on the scope.
ErrDefinedNameDuplicate = errors.New("the same name already exists on the scope")
// ErrCustomNumFmt defined the error message on receive the empty custom number format.
ErrCustomNumFmt = errors.New("custom number format can not be empty")
// ErrFontLength defined the error message on the length of the font
// family name overflow.
ErrFontLength = fmt.Errorf("the length of the font family name must be less than or equal to %d", MaxFontFamilyLength)
// ErrFontSize defined the error message on the size of the font is invalid.
ErrFontSize = fmt.Errorf("font size must be between %d and %d points", MinFontSize, MaxFontSize)
// ErrSheetIdx defined the error message on receive the invalid worksheet
// index.
ErrSheetIdx = errors.New("invalid worksheet index")
// ErrUnprotectSheet defined the error message on worksheet has set no
// protection.
ErrUnprotectSheet = errors.New("worksheet has set no protect")
// ErrUnprotectSheetPassword defined the error message on remove sheet
// protection with password verification failed.
ErrUnprotectSheetPassword = errors.New("worksheet protect password not match")
// ErrGroupSheets defined the error message on group sheets.
ErrGroupSheets = errors.New("group worksheet must contain an active worksheet")
// ErrDataValidationFormulaLength defined the error message for receiving a
// data validation formula length that exceeds the limit.
ErrDataValidationFormulaLength = fmt.Errorf("data validation must be 0-%d characters", MaxFieldLength)
// ErrDataValidationRange defined the error message on set decimal range
// exceeds limit.
ErrDataValidationRange = errors.New("data validation range exceeds limit")
// ErrCellCharsLength defined the error message for receiving a cell
// characters length that exceeds the limit.
ErrCellCharsLength = fmt.Errorf("cell value must be 0-%d characters", TotalCellChars)
// ErrOptionsUnzipSizeLimit defined the error message for receiving
// invalid UnzipSizeLimit and UnzipXMLSizeLimit.
ErrOptionsUnzipSizeLimit = errors.New("the value of UnzipSizeLimit should be greater than or equal to UnzipXMLSizeLimit")
// ErrSave defined the error message for saving file.
ErrSave = errors.New("no path defined for file, consider File.WriteTo or File.Write")
// ErrAttrValBool defined the error message on marshal and unmarshal
// boolean type XML attribute.
ErrAttrValBool = errors.New("unexpected child of attrValBool")
// ErrSparklineType defined the error message on receive the invalid
// sparkline Type parameters.
ErrSparklineType = errors.New("parameter 'Type' must be 'line', 'column' or 'win_loss'")
// ErrSparklineLocation defined the error message on missing Location
// parameters
ErrSparklineLocation = errors.New("parameter 'Location' is required")
// ErrSparklineRange defined the error message on missing sparkline Range
// parameters
ErrSparklineRange = errors.New("parameter 'Range' is required")
// ErrSparkline defined the error message on receive the invalid sparkline
// parameters.
ErrSparkline = errors.New("must have the same number of 'Location' and 'Range' parameters")
// ErrSparklineStyle defined the error message on receive the invalid
// sparkline Style parameters.
ErrSparklineStyle = errors.New("parameter 'Style' must between 0-35")
// ErrWorkbookPassword defined the error message on receiving the incorrect
// workbook password.
ErrWorkbookPassword = errors.New("the supplied open workbook password is not correct")
// ErrSheetNameInvalid defined the error message on receive the sheet name
// contains invalid characters.
ErrSheetNameInvalid = errors.New("the sheet can not contain any of the characters :\\/?*[or]")
// ErrSheetNameSingleQuote defined the error message on the first or last
// character of the sheet name was a single quote.
ErrSheetNameSingleQuote = errors.New("the first or last character of the sheet name can not be a single quote")
// ErrSheetNameBlank defined the error message on receive the blank sheet
// name.
ErrSheetNameBlank = errors.New("the sheet name can not be blank")
// ErrSheetNameLength defined the error message on receiving the sheet
// name length exceeds the limit.
ErrSheetNameLength = fmt.Errorf("the sheet name length exceeds the %d characters limit", MaxSheetNameLength)
// ErrTableNameLength defined the error message on receiving the table name
// length exceeds the limit.
ErrTableNameLength = fmt.Errorf("the table name length exceeds the %d characters limit", MaxFieldLength)
// ErrCellStyles defined the error message on cell styles exceeds the limit.
ErrCellStyles = fmt.Errorf("the cell styles exceeds the %d limit", MaxCellStyles)
// ErrUnprotectWorkbook defined the error message on workbook has set no
// protection.
ErrUnprotectWorkbook = errors.New("workbook has set no protect")
// ErrUnprotectWorkbookPassword defined the error message on remove workbook
// protection with password verification failed.
ErrUnprotectWorkbookPassword = errors.New("workbook protect password not match")
)

View File

@ -1,4 +1,4 @@
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in // this source code is governed by a BSD-style license that can be found in
// the LICENSE file. // the LICENSE file.
@ -7,7 +7,7 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming // Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of // API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.16 or later. // data. This library needs Go version 1.18 or later.
// //
// See https://xuri.me/excelize for more information about this package. // See https://xuri.me/excelize for more information about this package.
package excelize package excelize
@ -28,39 +28,42 @@ import (
// File define a populated spreadsheet file struct. // File define a populated spreadsheet file struct.
type File struct { type File struct {
sync.Mutex mu sync.Mutex
checked sync.Map
formulaChecked bool
options *Options options *Options
xmlAttr map[string][]xml.Attr sharedStringItem [][]uint
checked map[string]bool sharedStringsMap map[string]int
sharedStringTemp *os.File
sheetMap map[string]string sheetMap map[string]string
streams map[string]*StreamWriter streams map[string]*StreamWriter
tempFiles sync.Map tempFiles sync.Map
sharedStringsMap map[string]int xmlAttr sync.Map
sharedStringItem [][]uint
sharedStringTemp *os.File
CalcChain *xlsxCalcChain CalcChain *xlsxCalcChain
CharsetReader charsetTranscoderFn
Comments map[string]*xlsxComments Comments map[string]*xlsxComments
ContentTypes *xlsxTypes ContentTypes *xlsxTypes
DecodeVMLDrawing map[string]*decodeVmlDrawing
DecodeCellImages *decodeCellImages
Drawings sync.Map Drawings sync.Map
Path string Path string
Pkg sync.Map
Relationships sync.Map
SharedStrings *xlsxSST SharedStrings *xlsxSST
Sheet sync.Map Sheet sync.Map
SheetCount int SheetCount int
Styles *xlsxStyleSheet Styles *xlsxStyleSheet
Theme *xlsxTheme Theme *decodeTheme
DecodeVMLDrawing map[string]*decodeVmlDrawing
VMLDrawing map[string]*vmlDrawing VMLDrawing map[string]*vmlDrawing
VolatileDeps *xlsxVolTypes
WorkBook *xlsxWorkbook WorkBook *xlsxWorkbook
Relationships sync.Map
Pkg sync.Map
CharsetReader charsetTranscoderFn
} }
// charsetTranscoderFn set user-defined codepage transcoder function for open // charsetTranscoderFn set user-defined codepage transcoder function for open
// the spreadsheet from non-UTF-8 encoding. // the spreadsheet from non-UTF-8 encoding.
type charsetTranscoderFn func(charset string, input io.Reader) (rdr io.Reader, err error) type charsetTranscoderFn func(charset string, input io.Reader) (rdr io.Reader, err error)
// Options define the options for open and reading spreadsheet. // Options define the options for opening and reading the spreadsheet.
// //
// MaxCalcIterations specifies the maximum iterations for iterative // MaxCalcIterations specifies the maximum iterations for iterative
// calculation, the default value is 0. // calculation, the default value is 0.
@ -70,7 +73,7 @@ type charsetTranscoderFn func(charset string, input io.Reader) (rdr io.Reader, e
// RawCellValue specifies if apply the number format for the cell value or get // RawCellValue specifies if apply the number format for the cell value or get
// the raw value. // the raw value.
// //
// UnzipSizeLimit specifies the unzip size limit in bytes on open the // UnzipSizeLimit specifies to unzip size limit in bytes on open the
// spreadsheet, this value should be greater than or equal to // spreadsheet, this value should be greater than or equal to
// UnzipXMLSizeLimit, the default size limit is 16GB. // UnzipXMLSizeLimit, the default size limit is 16GB.
// //
@ -79,15 +82,34 @@ type charsetTranscoderFn func(charset string, input io.Reader) (rdr io.Reader, e
// temporary directory when the file size is over this value, this value // temporary directory when the file size is over this value, this value
// should be less than or equal to UnzipSizeLimit, the default value is // should be less than or equal to UnzipSizeLimit, the default value is
// 16MB. // 16MB.
//
// ShortDatePattern specifies the short date number format code. In the
// spreadsheet applications, date formats display date and time serial numbers
// as date values. Date formats that begin with an asterisk (*) respond to
// changes in regional date and time settings that are specified for the
// operating system. Formats without an asterisk are not affected by operating
// system settings. The ShortDatePattern used for specifies apply date formats
// that begin with an asterisk.
//
// LongDatePattern specifies the long date number format code.
//
// LongTimePattern specifies the long time number format code.
//
// CultureInfo specifies the country code for applying built-in language number
// format code these effect by the system's local language settings.
type Options struct { type Options struct {
MaxCalcIterations uint MaxCalcIterations uint
Password string Password string
RawCellValue bool RawCellValue bool
UnzipSizeLimit int64 UnzipSizeLimit int64
UnzipXMLSizeLimit int64 UnzipXMLSizeLimit int64
ShortDatePattern string
LongDatePattern string
LongTimePattern string
CultureInfo CultureName
} }
// OpenFile take the name of an spreadsheet file and returns a populated // OpenFile take the name of a spreadsheet file and returns a populated
// spreadsheet file struct for it. For example, open spreadsheet with // spreadsheet file struct for it. For example, open spreadsheet with
// password protection: // password protection:
// //
@ -101,11 +123,10 @@ func OpenFile(filename string, opts ...Options) (*File, error) {
} }
f, err := OpenReader(file, opts...) f, err := OpenReader(file, opts...)
if err != nil { if err != nil {
closeErr := file.Close() if closeErr := file.Close(); closeErr != nil {
if closeErr == nil { return f, closeErr
return f, err
} }
return f, closeErr return f, err
} }
f.Path = filename f.Path = filename
return f, file.Close() return f, file.Close()
@ -115,8 +136,8 @@ func OpenFile(filename string, opts ...Options) (*File, error) {
func newFile() *File { func newFile() *File {
return &File{ return &File{
options: &Options{UnzipSizeLimit: UnzipSizeLimit, UnzipXMLSizeLimit: StreamChunkSize}, options: &Options{UnzipSizeLimit: UnzipSizeLimit, UnzipXMLSizeLimit: StreamChunkSize},
xmlAttr: make(map[string][]xml.Attr), xmlAttr: sync.Map{},
checked: make(map[string]bool), checked: sync.Map{},
sheetMap: make(map[string]string), sheetMap: make(map[string]string),
tempFiles: sync.Map{}, tempFiles: sync.Map{},
Comments: make(map[string]*xlsxComments), Comments: make(map[string]*xlsxComments),
@ -148,7 +169,7 @@ func (f *File) checkOpenReaderOptions() error {
if f.options.UnzipXMLSizeLimit > f.options.UnzipSizeLimit { if f.options.UnzipXMLSizeLimit > f.options.UnzipSizeLimit {
return ErrOptionsUnzipSizeLimit return ErrOptionsUnzipSizeLimit
} }
return nil return f.checkDateTimePattern()
} }
// OpenReader read data stream from io.Reader and return a populated // OpenReader read data stream from io.Reader and return a populated
@ -159,7 +180,7 @@ func OpenReader(r io.Reader, opts ...Options) (*File, error) {
return nil, err return nil, err
} }
f := newFile() f := newFile()
f.options = getOptions(opts...) f.options = f.getOptions(opts...)
if err = f.checkOpenReaderOptions(); err != nil { if err = f.checkOpenReaderOptions(); err != nil {
return nil, err return nil, err
} }
@ -198,8 +219,8 @@ func OpenReader(r io.Reader, opts ...Options) (*File, error) {
// getOptions provides a function to parse the optional settings for open // getOptions provides a function to parse the optional settings for open
// and reading spreadsheet. // and reading spreadsheet.
func getOptions(opts ...Options) *Options { func (f *File) getOptions(opts ...Options) *Options {
options := &Options{} options := f.options
for _, opt := range opts { for _, opt := range opts {
options = &opt options = &opt
} }
@ -207,7 +228,7 @@ func getOptions(opts ...Options) *Options {
} }
// CharsetTranscoder Set user defined codepage transcoder function for open // CharsetTranscoder Set user defined codepage transcoder function for open
// XLSX from non UTF-8 encoding. // workbook from non UTF-8 encoding.
func (f *File) CharsetTranscoder(fn charsetTranscoderFn) *File { f.CharsetReader = fn; return f } func (f *File) CharsetTranscoder(fn charsetTranscoderFn) *File { f.CharsetReader = fn; return f }
// Creates new XML decoder with charset reader. // Creates new XML decoder with charset reader.
@ -221,22 +242,23 @@ func (f *File) xmlNewDecoder(rdr io.Reader) (ret *xml.Decoder) {
// time.Time type cell value by given worksheet name, cell reference and // time.Time type cell value by given worksheet name, cell reference and
// number format code. // number format code.
func (f *File) setDefaultTimeStyle(sheet, cell string, format int) error { func (f *File) setDefaultTimeStyle(sheet, cell string, format int) error {
s, err := f.GetCellStyle(sheet, cell) styleIdx, err := f.GetCellStyle(sheet, cell)
if err != nil { if err != nil {
return err return err
} }
if s == 0 { if styleIdx == 0 {
style, _ := f.NewStyle(&Style{NumFmt: format}) styleIdx, _ = f.NewStyle(&Style{NumFmt: format})
err = f.SetCellStyle(sheet, cell, cell, style) } else {
style, _ := f.GetStyle(styleIdx)
style.NumFmt = format
styleIdx, _ = f.NewStyle(style)
} }
return err return f.SetCellStyle(sheet, cell, cell, styleIdx)
} }
// workSheetReader provides a function to get the pointer to the structure // workSheetReader provides a function to get the pointer to the structure
// after deserialization by given worksheet name. // after deserialization by given worksheet name.
func (f *File) workSheetReader(sheet string) (ws *xlsxWorksheet, err error) { func (f *File) workSheetReader(sheet string) (ws *xlsxWorksheet, err error) {
f.Lock()
defer f.Unlock()
var ( var (
name string name string
ok bool ok bool
@ -245,7 +267,7 @@ func (f *File) workSheetReader(sheet string) (ws *xlsxWorksheet, err error) {
return return
} }
if name, ok = f.getSheetXMLPath(sheet); !ok { if name, ok = f.getSheetXMLPath(sheet); !ok {
err = newNoExistSheetError(sheet) err = ErrSheetNotExist{sheet}
return return
} }
if worksheet, ok := f.Sheet.Load(name); ok && worksheet != nil { if worksheet, ok := f.Sheet.Load(name); ok && worksheet != nil {
@ -259,24 +281,25 @@ func (f *File) workSheetReader(sheet string) (ws *xlsxWorksheet, err error) {
} }
} }
ws = new(xlsxWorksheet) ws = new(xlsxWorksheet)
if _, ok := f.xmlAttr[name]; !ok { if attrs, ok := f.xmlAttr.Load(name); !ok {
d := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readBytes(name)))) d := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readBytes(name))))
f.xmlAttr[name] = append(f.xmlAttr[name], getRootElement(d)...) if attrs == nil {
attrs = []xml.Attr{}
}
attrs = append(attrs.([]xml.Attr), getRootElement(d)...)
f.xmlAttr.Store(name, attrs)
} }
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readBytes(name)))). if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readBytes(name)))).
Decode(ws); err != nil && err != io.EOF { Decode(ws); err != nil && err != io.EOF {
return return
} }
err = nil err = nil
if f.checked == nil { if _, ok = f.checked.Load(name); !ok {
f.checked = make(map[string]bool) ws.checkSheet()
} if err = ws.checkRow(); err != nil {
if ok = f.checked[name]; !ok {
checkSheet(ws)
if err = checkRow(ws); err != nil {
return return
} }
f.checked[name] = true f.checked.Store(name, true)
} }
f.Sheet.Store(name, ws) f.Sheet.Store(name, ws)
return return
@ -284,62 +307,86 @@ func (f *File) workSheetReader(sheet string) (ws *xlsxWorksheet, err error) {
// checkSheet provides a function to fill each row element and make that is // checkSheet provides a function to fill each row element and make that is
// continuous in a worksheet of XML. // continuous in a worksheet of XML.
func checkSheet(ws *xlsxWorksheet) { func (ws *xlsxWorksheet) checkSheet() {
var row int var (
var r0 xlsxRow row int
for i, r := range ws.SheetData.Row { r0Rows []xlsxRow
if i == 0 && r.R == 0 { lastRowNum = func(r xlsxRow) int {
r0 = r var num int
ws.SheetData.Row = ws.SheetData.Row[1:] for _, cell := range r.C {
if _, row, err := CellNameToCoordinates(cell.R); err == nil {
if row > num {
num = row
}
}
}
return num
}
)
for i := 0; i < len(ws.SheetData.Row); i++ {
r := ws.SheetData.Row[i]
if r.R == 0 || r.R == row {
num := lastRowNum(r)
if num > row {
row = num
}
if num == 0 {
row++
}
r.R = row
r0Rows = append(r0Rows, r)
ws.SheetData.Row = append(ws.SheetData.Row[:i], ws.SheetData.Row[i+1:]...)
i--
continue continue
} }
if r.R != 0 && r.R > row { if r.R != 0 && r.R > row {
row = r.R row = r.R
continue
}
if r.R != row {
row++
} }
} }
sheetData := xlsxSheetData{Row: make([]xlsxRow, row)} sheetData := xlsxSheetData{Row: make([]xlsxRow, row)}
row = 0 row = 0
for _, r := range ws.SheetData.Row { for _, r := range ws.SheetData.Row {
if r.R == row && row > 0 {
sheetData.Row[r.R-1].C = append(sheetData.Row[r.R-1].C, r.C...)
continue
}
if r.R != 0 { if r.R != 0 {
sheetData.Row[r.R-1] = r sheetData.Row[r.R-1] = r
row = r.R row = r.R
continue
} }
row++ }
r.R = row for _, r0Row := range r0Rows {
sheetData.Row[row-1] = r sheetData.Row[r0Row.R-1].R = r0Row.R
ws.checkSheetR0(&sheetData, &r0Row, true)
} }
for i := 1; i <= row; i++ { for i := 1; i <= row; i++ {
sheetData.Row[i-1].R = i sheetData.Row[i-1].R = i
ws.checkSheetR0(&sheetData, &sheetData.Row[i-1], false)
} }
checkSheetR0(ws, &sheetData, &r0)
} }
// checkSheetR0 handle the row element with r="0" attribute, cells in this row // checkSheetR0 handle the row element with r="0" attribute, cells in this row
// could be disorderly, the cell in this row can be used as the value of // could be disorderly, the cell in this row can be used as the value of
// which cell is empty in the normal rows. // which cell is empty in the normal rows.
func checkSheetR0(ws *xlsxWorksheet, sheetData *xlsxSheetData, r0 *xlsxRow) { func (ws *xlsxWorksheet) checkSheetR0(sheetData *xlsxSheetData, rowData *xlsxRow, r0 bool) {
for _, cell := range r0.C { checkRow := func(col, row int, r0 bool, cell xlsxC) {
if col, row, err := CellNameToCoordinates(cell.R); err == nil { rowIdx := row - 1
rows, rowIdx := len(sheetData.Row), row-1 columns, colIdx := len(sheetData.Row[rowIdx].C), col-1
for r := rows; r < row; r++ { for c := columns; c < col; c++ {
sheetData.Row = append(sheetData.Row, xlsxRow{R: r + 1}) sheetData.Row[rowIdx].C = append(sheetData.Row[rowIdx].C, xlsxC{})
} }
columns, colIdx := len(sheetData.Row[rowIdx].C), col-1 if !sheetData.Row[rowIdx].C[colIdx].hasValue() {
for c := columns; c < col; c++ { sheetData.Row[rowIdx].C[colIdx] = cell
sheetData.Row[rowIdx].C = append(sheetData.Row[rowIdx].C, xlsxC{}) }
} if r0 {
if !sheetData.Row[rowIdx].C[colIdx].hasValue() { sheetData.Row[rowIdx].C[colIdx] = cell
sheetData.Row[rowIdx].C[colIdx] = cell }
} }
var err error
for i, cell := range rowData.C {
col, row := i+1, rowData.R
if cell.R == "" {
checkRow(col, row, r0, cell)
continue
}
if col, row, err = CellNameToCoordinates(cell.R); err == nil && r0 {
checkRow(col, row, r0, cell)
} }
} }
ws.SheetData = *sheetData ws.SheetData = *sheetData
@ -352,8 +399,8 @@ func (f *File) setRels(rID, relPath, relType, target, targetMode string) int {
if rels == nil || rID == "" { if rels == nil || rID == "" {
return f.addRels(relPath, relType, target, targetMode) return f.addRels(relPath, relType, target, targetMode)
} }
rels.Lock() rels.mu.Lock()
defer rels.Unlock() defer rels.mu.Unlock()
var ID int var ID int
for i, rel := range rels.Relationships { for i, rel := range rels.Relationships {
if rel.ID == rID { if rel.ID == rID {
@ -377,8 +424,8 @@ func (f *File) addRels(relPath, relType, target, targetMode string) int {
if rels == nil { if rels == nil {
rels = &xlsxRelationships{} rels = &xlsxRelationships{}
} }
rels.Lock() rels.mu.Lock()
defer rels.Unlock() defer rels.mu.Unlock()
var rID int var rID int
for idx, rel := range rels.Relationships { for idx, rel := range rels.Relationships {
ID, _ := strconv.Atoi(strings.TrimPrefix(rel.ID, "rId")) ID, _ := strconv.Atoi(strings.TrimPrefix(rel.ID, "rId"))
@ -409,14 +456,14 @@ func (f *File) addRels(relPath, relType, target, targetMode string) int {
// UpdateLinkedValue fix linked values within a spreadsheet are not updating in // UpdateLinkedValue fix linked values within a spreadsheet are not updating in
// Office Excel application. This function will be remove value tag when met a // Office Excel application. This function will be remove value tag when met a
// cell have a linked value. Reference // cell have a linked value. Reference
// https://social.technet.microsoft.com/Forums/office/en-US/e16bae1f-6a2c-4325-8013-e989a3479066/excel-2010-linked-cells-not-updating // https://learn.microsoft.com/en-us/archive/msdn-technet-forums/e16bae1f-6a2c-4325-8013-e989a3479066
// //
// Notice: after opening generated workbook, Excel will update the linked value // Notice: after opening generated workbook, Excel will update the linked value
// and generate a new value and will prompt to save the file or not. // and generate a new value and will prompt to save the file or not.
// //
// For example: // For example:
// //
// <row r="19" spans="2:2"> // <row r="19">
// <c r="B19"> // <c r="B19">
// <f>SUM(Sheet2!D2,Sheet2!D11)</f> // <f>SUM(Sheet2!D2,Sheet2!D11)</f>
// <v>100</v> // <v>100</v>
@ -425,7 +472,7 @@ func (f *File) addRels(relPath, relType, target, targetMode string) int {
// //
// to // to
// //
// <row r="19" spans="2:2"> // <row r="19">
// <c r="B19"> // <c r="B19">
// <f>SUM(Sheet2!D2,Sheet2!D11)</f> // <f>SUM(Sheet2!D2,Sheet2!D11)</f>
// </c> // </c>
@ -491,8 +538,8 @@ func (f *File) AddVBAProject(file []byte) error {
if err != nil { if err != nil {
return err return err
} }
rels.Lock() rels.mu.Lock()
defer rels.Unlock() defer rels.mu.Unlock()
var rID int var rID int
var ok bool var ok bool
for _, rel := range rels.Relationships { for _, rel := range rels.Relationships {
@ -525,8 +572,8 @@ func (f *File) setContentTypePartProjectExtensions(contentType string) error {
if err != nil { if err != nil {
return err return err
} }
content.Lock() content.mu.Lock()
defer content.Unlock() defer content.mu.Unlock()
for _, v := range content.Defaults { for _, v := range content.Defaults {
if v.Extension == "bin" { if v.Extension == "bin" {
ok = true ok = true
@ -545,3 +592,77 @@ func (f *File) setContentTypePartProjectExtensions(contentType string) error {
} }
return err return err
} }
// metadataReader provides a function to get the pointer to the structure
// after deserialization of xl/metadata.xml.
func (f *File) metadataReader() (*xlsxMetadata, error) {
var mataData xlsxMetadata
if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLMetadata)))).
Decode(&mataData); err != nil && err != io.EOF {
return &mataData, err
}
return &mataData, nil
}
// richValueReader provides a function to get the pointer to the structure after
// deserialization of xl/richData/richvalue.xml.
func (f *File) richValueReader() (*xlsxRichValueData, error) {
var richValue xlsxRichValueData
if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLRdRichValuePart)))).
Decode(&richValue); err != nil && err != io.EOF {
return &richValue, err
}
return &richValue, nil
}
// richValueRelReader provides a function to get the pointer to the structure
// after deserialization of xl/richData/richValueRel.xml.
func (f *File) richValueRelReader() (*xlsxRichValueRels, error) {
var richValueRels xlsxRichValueRels
if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLRdRichValueRel)))).
Decode(&richValueRels); err != nil && err != io.EOF {
return &richValueRels, err
}
return &richValueRels, nil
}
// richValueWebImageReader provides a function to get the pointer to the
// structure after deserialization of xl/richData/rdRichValueWebImage.xml.
func (f *File) richValueWebImageReader() (*xlsxWebImagesSupportingRichData, error) {
var richValueWebImages xlsxWebImagesSupportingRichData
if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLRdRichValueWebImagePart)))).
Decode(&richValueWebImages); err != nil && err != io.EOF {
return &richValueWebImages, err
}
return &richValueWebImages, nil
}
// getRichDataRichValueRelRelationships provides a function to get relationships
// from xl/richData/_rels/richValueRel.xml.rels by given relationship ID.
func (f *File) getRichDataRichValueRelRelationships(rID string) *xlsxRelationship {
if rels, _ := f.relsReader(defaultXMLRdRichValueRelRels); rels != nil {
rels.mu.Lock()
defer rels.mu.Unlock()
for _, v := range rels.Relationships {
if v.ID == rID {
return &v
}
}
}
return nil
}
// getRichValueWebImageRelationships provides a function to get relationships
// from xl/richData/_rels/rdRichValueWebImage.xml.rels by given relationship ID.
func (f *File) getRichValueWebImageRelationships(rID string) *xlsxRelationship {
if rels, _ := f.relsReader(defaultXMLRdRichValueWebImagePartRels); rels != nil {
rels.mu.Lock()
defer rels.mu.Unlock()
for _, v := range rels.Relationships {
if v.ID == rID {
return &v
}
}
}
return nil
}

View File

@ -16,6 +16,7 @@ import (
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"sync"
"testing" "testing"
"time" "time"
@ -207,6 +208,30 @@ func TestSaveFile(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.NoError(t, f.Save()) assert.NoError(t, f.Save())
assert.NoError(t, f.Close()) assert.NoError(t, f.Close())
t.Run("for_save_multiple_times", func(t *testing.T) {
{
f, err := OpenFile(filepath.Join("test", "TestSaveFile.xlsx"))
assert.NoError(t, err)
assert.NoError(t, f.SetCellValue("Sheet1", "A20", 20))
assert.NoError(t, f.Save())
assert.NoError(t, f.SetCellValue("Sheet1", "A21", 21))
assert.NoError(t, f.Save())
assert.NoError(t, f.Close())
}
{
f, err := OpenFile(filepath.Join("test", "TestSaveFile.xlsx"))
assert.NoError(t, err)
val, err := f.GetCellValue("Sheet1", "A20")
assert.NoError(t, err)
assert.Equal(t, "20", val)
val, err = f.GetCellValue("Sheet1", "A21")
assert.NoError(t, err)
assert.Equal(t, "21", val)
assert.NoError(t, f.Close())
}
})
} }
func TestSaveAsWrongPath(t *testing.T) { func TestSaveAsWrongPath(t *testing.T) {
@ -364,11 +389,11 @@ func TestNewFile(t *testing.T) {
f := NewFile() f := NewFile()
_, err := f.NewSheet("Sheet1") _, err := f.NewSheet("Sheet1")
assert.NoError(t, err) assert.NoError(t, err)
_, err = f.NewSheet("XLSXSheet2") _, err = f.NewSheet("Sheet2")
assert.NoError(t, err) assert.NoError(t, err)
_, err = f.NewSheet("XLSXSheet3") _, err = f.NewSheet("Sheet3")
assert.NoError(t, err) assert.NoError(t, err)
assert.NoError(t, f.SetCellInt("XLSXSheet2", "A23", 56)) assert.NoError(t, f.SetCellInt("Sheet2", "A23", 56))
assert.NoError(t, f.SetCellStr("Sheet1", "B20", "42")) assert.NoError(t, f.SetCellStr("Sheet1", "B20", "42"))
f.SetActiveSheet(0) f.SetActiveSheet(0)
@ -383,15 +408,6 @@ func TestNewFile(t *testing.T) {
assert.NoError(t, f.Save()) assert.NoError(t, f.Save())
} }
func TestAddDrawingVML(t *testing.T) {
// Test addDrawingVML with illegal cell reference
f := NewFile()
assert.EqualError(t, f.addDrawingVML(0, "", "*", 0, 0), newCellNameToCoordinatesError("*", newInvalidCellNameError("*")).Error())
f.Pkg.Store("xl/drawings/vmlDrawing1.vml", MacintoshCyrillicCharset)
assert.EqualError(t, f.addDrawingVML(0, "xl/drawings/vmlDrawing1.vml", "A1", 0, 0), "XML syntax error on line 1: invalid UTF-8")
}
func TestSetCellHyperLink(t *testing.T) { func TestSetCellHyperLink(t *testing.T) {
f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
assert.NoError(t, err) assert.NoError(t, err)
@ -408,8 +424,8 @@ func TestSetCellHyperLink(t *testing.T) {
Tooltip: &tooltip, Tooltip: &tooltip,
})) }))
// Test set cell hyperlink with invalid sheet name // Test set cell hyperlink with invalid sheet name
assert.EqualError(t, f.SetCellHyperLink("Sheet:1", "A1", "Sheet1!D60", "Location"), ErrSheetNameInvalid.Error()) assert.Equal(t, ErrSheetNameInvalid, f.SetCellHyperLink("Sheet:1", "A1", "Sheet1!D60", "Location"))
assert.EqualError(t, f.SetCellHyperLink("Sheet2", "C3", "Sheet1!D8", ""), `invalid link type ""`) assert.Equal(t, newInvalidLinkTypeError(""), f.SetCellHyperLink("Sheet2", "C3", "Sheet1!D8", ""))
assert.EqualError(t, f.SetCellHyperLink("Sheet2", "", "Sheet1!D60", "Location"), `invalid cell name ""`) assert.EqualError(t, f.SetCellHyperLink("Sheet2", "", "Sheet1!D60", "Location"), `invalid cell name ""`)
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellHyperLink.xlsx"))) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellHyperLink.xlsx")))
assert.NoError(t, f.Close()) assert.NoError(t, f.Close())
@ -439,6 +455,18 @@ func TestSetCellHyperLink(t *testing.T) {
assert.Equal(t, link, true) assert.Equal(t, link, true)
assert.Equal(t, "https://github.com/xuri/excelize", target) assert.Equal(t, "https://github.com/xuri/excelize", target)
assert.NoError(t, err) assert.NoError(t, err)
// Test remove hyperlink for a cell
f = NewFile()
assert.NoError(t, f.SetCellHyperLink("Sheet1", "A1", "Sheet1!D8", "Location"))
ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml")
assert.True(t, ok)
ws.(*xlsxWorksheet).Hyperlinks.Hyperlink[0].Ref = "A1:D4"
assert.NoError(t, f.SetCellHyperLink("Sheet1", "B2", "", "None"))
// Test remove hyperlink for a cell with invalid cell reference
assert.NoError(t, f.SetCellHyperLink("Sheet1", "A1", "Sheet1!D8", "Location"))
ws.(*xlsxWorksheet).Hyperlinks.Hyperlink[0].Ref = "A:A"
assert.Error(t, f.SetCellHyperLink("Sheet1", "B2", "", "None"), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")))
} }
func TestGetCellHyperLink(t *testing.T) { func TestGetCellHyperLink(t *testing.T) {
@ -747,33 +775,33 @@ func TestSetCellStyleNumberFormat(t *testing.T) {
// Test only set fill and number format for a cell // Test only set fill and number format for a cell
col := []string{"L", "M", "N", "O", "P"} col := []string{"L", "M", "N", "O", "P"}
data := []int{0, 1, 2, 3, 4, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49} idxTbl := []int{0, 1, 2, 3, 4, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49}
value := []string{"37947.7500001", "-37947.7500001", "0.007", "2.1", "String"} value := []string{"37947.7500001", "-37947.7500001", "0.007", "2.1", "String"}
expected := [][]string{ expected := [][]string{
{"37947.7500001", "37948", "37947.75", "37,948", "37947.75", "3794775%", "3794775.00%", "3.79E+04", "37947.7500001", "37947.7500001", "11-22-03", "22-Nov-03", "22-Nov", "Nov-03", "6:00 pm", "6:00:00 pm", "18:00", "18:00:00", "11/22/03 18:00", "37,948 ", "37,948 ", "37,947.75 ", "37,947.75 ", "37947.7500001", "37947.7500001", "37947.7500001", "37947.7500001", "00:00", "910746:00:00", "37947.7500001", "3.79E+04", "37947.7500001"}, {"37947.75", "37948", "37947.75", "37,948", "37,947.75", "3794775%", "3794775.00%", "3.79E+04", "37947 3/4", "37947 3/4", "11-22-03", "22-Nov-03", "22-Nov", "Nov-03", "6:00 PM", "6:00:00 PM", "18:00", "18:00:00", "11/22/03 18:00", "37,948 ", "37,948 ", "37,947.75 ", "37,947.75 ", " 37,948 ", " $37,948 ", " 37,947.75 ", " $37,947.75 ", "00:00", "910746:00:00", "00:00.0", "37947.7500001", "37947.7500001"},
{"-37947.7500001", "-37948", "-37947.75", "-37,948", "-37947.75", "-3794775%", "-3794775.00%", "-3.79E+04", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "(37,948)", "(37,948)", "(37,947.75)", "(37,947.75)", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-3.79E+04", "-37947.7500001"}, {"-37947.75", "-37948", "-37947.75", "-37,948", "-37,947.75", "-3794775%", "-3794775.00%", "-3.79E+04", "-37947 3/4", "-37947 3/4", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "(37,948)", "(37,948)", "(37,947.75)", "(37,947.75)", " (37,948)", " $(37,948)", " (37,947.75)", " $(37,947.75)", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001", "-37947.7500001"},
{"0.007", "0", "0.01", "0", "0.01", "1%", "0.70%", "7.00E-03", "0.007", "0.007", "12-30-99", "30-Dec-99", "30-Dec", "Dec-99", "0:10 am", "0:10:04 am", "00:10", "00:10:04", "12/30/99 00:10", "0 ", "0 ", "0.01 ", "0.01 ", "0.007", "0.007", "0.007", "0.007", "10:04", "0:10:04", "0.007", "7.00E-03", "0.007"}, {"0.007", "0", "0.01", "0", "0.01", "1%", "0.70%", "7.00E-03", "0 ", "0 ", "12-30-99", "30-Dec-99", "30-Dec", "Dec-99", "12:10 AM", "12:10:05 AM", "00:10", "00:10:05", "12/30/99 00:10", "0 ", "0 ", "0.01 ", "0.01 ", " 0 ", " $0 ", " 0.01 ", " $0.01 ", "10:05", "0:10:05", "10:04.8", "0.007", "0.007"},
{"2.1", "2", "2.10", "2", "2.10", "210%", "210.00%", "2.10E+00", "2.1", "2.1", "01-01-00", "1-Jan-00", "1-Jan", "Jan-00", "2:24 am", "2:24:00 am", "02:24", "02:24:00", "1/1/00 02:24", "2 ", "2 ", "2.10 ", "2.10 ", "2.1", "2.1", "2.1", "2.1", "24:00", "50:24:00", "2.1", "2.10E+00", "2.1"}, {"2.1", "2", "2.10", "2", "2.10", "210%", "210.00%", "2.10E+00", "2 1/9", "2 1/10", "01-01-00", "1-Jan-00", "1-Jan", "Jan-00", "2:24 AM", "2:24:00 AM", "02:24", "02:24:00", "1/1/00 02:24", "2 ", "2 ", "2.10 ", "2.10 ", " 2 ", " $2 ", " 2.10 ", " $2.10 ", "24:00", "50:24:00", "24:00.0", "2.1", "2.1"},
{"String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String"}, {"String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", "String", " String ", " String ", " String ", " String ", "String", "String", "String", "String", "String"},
} }
for i, v := range value { for c, v := range value {
for k, d := range data { for r, idx := range idxTbl {
c := col[i] + strconv.Itoa(k+1) cell := col[c] + strconv.Itoa(r+1)
var val float64 var val float64
val, err = strconv.ParseFloat(v, 64) val, err = strconv.ParseFloat(v, 64)
if err != nil { if err != nil {
assert.NoError(t, f.SetCellValue("Sheet2", c, v)) assert.NoError(t, f.SetCellValue("Sheet2", cell, v))
} else { } else {
assert.NoError(t, f.SetCellValue("Sheet2", c, val)) assert.NoError(t, f.SetCellValue("Sheet2", cell, val))
} }
style, err := f.NewStyle(&Style{Fill: Fill{Type: "gradient", Color: []string{"FFFFFF", "E0EBF5"}, Shading: 5}, NumFmt: d}) style, err := f.NewStyle(&Style{Fill: Fill{Type: "gradient", Color: []string{"FFFFFF", "E0EBF5"}, Shading: 5}, NumFmt: idx})
if !assert.NoError(t, err) { if !assert.NoError(t, err) {
t.FailNow() t.FailNow()
} }
assert.NoError(t, f.SetCellStyle("Sheet2", c, c, style)) assert.NoError(t, f.SetCellStyle("Sheet2", cell, cell, style))
cellValue, err := f.GetCellValue("Sheet2", c) cellValue, err := f.GetCellValue("Sheet2", cell)
assert.Equal(t, expected[i][k], cellValue, "Sheet2!"+c, i, k) assert.Equal(t, expected[c][r], cellValue, fmt.Sprintf("Sheet2!%s value: %s, number format: %s c: %d r: %d", cell, value[c], builtInNumFmt[idx], c, r))
assert.NoError(t, err) assert.NoError(t, err)
} }
} }
@ -783,6 +811,16 @@ func TestSetCellStyleNumberFormat(t *testing.T) {
assert.NoError(t, f.SetCellStyle("Sheet2", "L33", "L33", style)) assert.NoError(t, f.SetCellStyle("Sheet2", "L33", "L33", style))
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellStyleNumberFormat.xlsx"))) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetCellStyleNumberFormat.xlsx")))
// Test get cell value with built-in number format code 22 with custom short date pattern
f = NewFile(Options{ShortDatePattern: "yyyy-m-dd"})
assert.NoError(t, f.SetCellValue("Sheet1", "A1", 45074.625694444447))
style, err = f.NewStyle(&Style{NumFmt: 22})
assert.NoError(t, err)
assert.NoError(t, f.SetCellStyle("Sheet1", "A1", "A1", style))
cellValue, err := f.GetCellValue("Sheet1", "A1")
assert.NoError(t, err)
assert.Equal(t, "2023-5-28 15:01", cellValue)
} }
func TestSetCellStyleCurrencyNumberFormat(t *testing.T) { func TestSetCellStyleCurrencyNumberFormat(t *testing.T) {
@ -793,11 +831,11 @@ func TestSetCellStyleCurrencyNumberFormat(t *testing.T) {
assert.NoError(t, f.SetCellValue("Sheet1", "A1", 56)) assert.NoError(t, f.SetCellValue("Sheet1", "A1", 56))
assert.NoError(t, f.SetCellValue("Sheet1", "A2", -32.3)) assert.NoError(t, f.SetCellValue("Sheet1", "A2", -32.3))
var style int var style int
style, err = f.NewStyle(&Style{NumFmt: 188, DecimalPlaces: -1}) style, err = f.NewStyle(&Style{NumFmt: 188, DecimalPlaces: intPtr(-1)})
assert.NoError(t, err) assert.NoError(t, err)
assert.NoError(t, f.SetCellStyle("Sheet1", "A1", "A1", style)) assert.NoError(t, f.SetCellStyle("Sheet1", "A1", "A1", style))
style, err = f.NewStyle(&Style{NumFmt: 188, DecimalPlaces: 31, NegRed: true}) style, err = f.NewStyle(&Style{NumFmt: 188, DecimalPlaces: intPtr(31), NegRed: true})
assert.NoError(t, err) assert.NoError(t, err)
assert.NoError(t, f.SetCellStyle("Sheet1", "A2", "A2", style)) assert.NoError(t, f.SetCellStyle("Sheet1", "A2", "A2", style))
@ -811,19 +849,19 @@ func TestSetCellStyleCurrencyNumberFormat(t *testing.T) {
assert.NoError(t, f.SetCellValue("Sheet1", "A1", 42920.5)) assert.NoError(t, f.SetCellValue("Sheet1", "A1", 42920.5))
assert.NoError(t, f.SetCellValue("Sheet1", "A2", 42920.5)) assert.NoError(t, f.SetCellValue("Sheet1", "A2", 42920.5))
_, err = f.NewStyle(&Style{NumFmt: 26, Lang: "zh-tw"}) _, err = f.NewStyle(&Style{NumFmt: 26})
assert.NoError(t, err) assert.NoError(t, err)
style, err := f.NewStyle(&Style{NumFmt: 27}) style, err := f.NewStyle(&Style{NumFmt: 27})
assert.NoError(t, err) assert.NoError(t, err)
assert.NoError(t, f.SetCellStyle("Sheet1", "A1", "A1", style)) assert.NoError(t, f.SetCellStyle("Sheet1", "A1", "A1", style))
style, err = f.NewStyle(&Style{NumFmt: 31, Lang: "ko-kr"}) style, err = f.NewStyle(&Style{NumFmt: 31})
assert.NoError(t, err) assert.NoError(t, err)
assert.NoError(t, f.SetCellStyle("Sheet1", "A2", "A2", style)) assert.NoError(t, f.SetCellStyle("Sheet1", "A2", "A2", style))
style, err = f.NewStyle(&Style{NumFmt: 71, Lang: "th-th"}) style, err = f.NewStyle(&Style{NumFmt: 71})
assert.NoError(t, err) assert.NoError(t, err)
assert.NoError(t, f.SetCellStyle("Sheet1", "A2", "A2", style)) assert.NoError(t, f.SetCellStyle("Sheet1", "A2", "A2", style))
@ -831,6 +869,50 @@ func TestSetCellStyleCurrencyNumberFormat(t *testing.T) {
}) })
} }
func TestSetCellStyleLangNumberFormat(t *testing.T) {
rawCellValues := make([][]string, 42)
for i := 0; i < 42; i++ {
rawCellValues[i] = []string{"45162"}
}
for lang, expected := range map[CultureName][][]string{
CultureNameUnknown: rawCellValues,
CultureNameEnUS: {{"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"0:00:00"}, {"0:00:00"}, {"0:00:00"}, {"0:00:00"}, {"45162"}, {"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"8/24/23"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}},
CultureNameJaJP: {{"R5.8.24"}, {"令和5年8月24日"}, {"令和5年8月24日"}, {"8/24/23"}, {"2023年8月24日"}, {"0時00分"}, {"0時00分00秒"}, {"2023年8月"}, {"8月24日"}, {"R5.8.24"}, {"R5.8.24"}, {"令和5年8月24日"}, {"2023年8月"}, {"8月24日"}, {"令和5年8月24日"}, {"2023年8月"}, {"8月24日"}, {"R5.8.24"}, {"令和5年8月24日"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}},
CultureNameKoKR: {{"4356年 08月 24日"}, {"08-24"}, {"08-24"}, {"08-24-56"}, {"4356년 08월 24일"}, {"0시 00분"}, {"0시 00분 00초"}, {"4356-08-24"}, {"4356-08-24"}, {"4356年 08月 24日"}, {"4356年 08月 24日"}, {"08-24"}, {"4356-08-24"}, {"4356-08-24"}, {"08-24"}, {"4356-08-24"}, {"4356-08-24"}, {"4356年 08月 24日"}, {"08-24"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}},
CultureNameZhCN: {{"2023年8月"}, {"8月24日"}, {"8月24日"}, {"8/24/23"}, {"2023年8月24日"}, {"0时00分"}, {"0时00分00秒"}, {"上午12时00分"}, {"上午12时00分00秒"}, {"2023年8月"}, {"2023年8月"}, {"8月24日"}, {"2023年8月"}, {"8月24日"}, {"8月24日"}, {"上午12时00分"}, {"上午12时00分00秒"}, {"2023年8月"}, {"8月24日"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}},
CultureNameZhTW: {{"112/8/24"}, {"112年8月24日"}, {"112年8月24日"}, {"8/24/23"}, {"2023年8月24日"}, {"00時00分"}, {"00時00分00秒"}, {"上午12時00分"}, {"上午12時00分00秒"}, {"112/8/24"}, {"112/8/24"}, {"112年8月24日"}, {"上午12時00分"}, {"上午12時00分00秒"}, {"112年8月24日"}, {"上午12時00分"}, {"上午12時00分00秒"}, {"112/8/24"}, {"112年8月24日"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}},
} {
f, err := prepareTestBook5(Options{CultureInfo: lang})
assert.NoError(t, err)
rows, err := f.GetRows("Sheet1")
assert.NoError(t, err)
assert.Equal(t, expected, rows)
assert.NoError(t, f.Close())
}
// Test apply language number format code with date and time pattern
for lang, expected := range map[CultureName][][]string{
CultureNameEnUS: {{"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"00:00:00"}, {"00:00:00"}, {"00:00:00"}, {"00:00:00"}, {"45162"}, {"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"2023-8-24"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}},
CultureNameJaJP: {{"R5.8.24"}, {"令和5年8月24日"}, {"令和5年8月24日"}, {"2023-8-24"}, {"2023年8月24日"}, {"00:00:00"}, {"00:00:00"}, {"2023年8月"}, {"8月24日"}, {"R5.8.24"}, {"R5.8.24"}, {"令和5年8月24日"}, {"2023年8月"}, {"8月24日"}, {"令和5年8月24日"}, {"2023年8月"}, {"8月24日"}, {"R5.8.24"}, {"令和5年8月24日"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}},
CultureNameKoKR: {{"4356年 08月 24日"}, {"08-24"}, {"08-24"}, {"4356-8-24"}, {"4356년 08월 24일"}, {"00:00:00"}, {"00:00:00"}, {"4356-08-24"}, {"4356-08-24"}, {"4356年 08月 24日"}, {"4356年 08月 24日"}, {"08-24"}, {"4356-08-24"}, {"4356-08-24"}, {"08-24"}, {"4356-08-24"}, {"4356-08-24"}, {"4356年 08月 24日"}, {"08-24"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}},
CultureNameZhCN: {{"2023年8月"}, {"8月24日"}, {"8月24日"}, {"2023-8-24"}, {"2023年8月24日"}, {"00:00:00"}, {"00:00:00"}, {"上午12时00分"}, {"上午12时00分00秒"}, {"2023年8月"}, {"2023年8月"}, {"8月24日"}, {"2023年8月"}, {"8月24日"}, {"8月24日"}, {"上午12时00分"}, {"上午12时00分00秒"}, {"2023年8月"}, {"8月24日"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}},
CultureNameZhTW: {{"112/8/24"}, {"112年8月24日"}, {"112年8月24日"}, {"2023-8-24"}, {"2023年8月24日"}, {"00:00:00"}, {"00:00:00"}, {"上午12時00分"}, {"上午12時00分00秒"}, {"112/8/24"}, {"112/8/24"}, {"112年8月24日"}, {"上午12時00分"}, {"上午12時00分00秒"}, {"112年8月24日"}, {"上午12時00分"}, {"上午12時00分00秒"}, {"112/8/24"}, {"112年8月24日"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}},
} {
f, err := prepareTestBook5(Options{CultureInfo: lang, ShortDatePattern: "yyyy-M-d", LongTimePattern: "hh:mm:ss"})
assert.NoError(t, err)
rows, err := f.GetRows("Sheet1")
assert.NoError(t, err)
assert.Equal(t, expected, rows)
assert.NoError(t, f.Close())
}
// Test open workbook with invalid date and time pattern options
_, err := OpenFile(filepath.Join("test", "Book1.xlsx"), Options{LongDatePattern: "0.00"})
assert.Equal(t, ErrUnsupportedNumberFormat, err)
_, err = OpenFile(filepath.Join("test", "Book1.xlsx"), Options{LongTimePattern: "0.00"})
assert.Equal(t, ErrUnsupportedNumberFormat, err)
_, err = OpenFile(filepath.Join("test", "Book1.xlsx"), Options{ShortDatePattern: "0.00"})
assert.Equal(t, ErrUnsupportedNumberFormat, err)
}
func TestSetCellStyleCustomNumberFormat(t *testing.T) { func TestSetCellStyleCustomNumberFormat(t *testing.T) {
f := NewFile() f := NewFile()
assert.NoError(t, f.SetCellValue("Sheet1", "A1", 42920.5)) assert.NoError(t, f.SetCellValue("Sheet1", "A1", 42920.5))
@ -925,7 +1007,7 @@ func TestSetDeleteSheet(t *testing.T) {
f, err := prepareTestBook3() f, err := prepareTestBook3()
assert.NoError(t, err) assert.NoError(t, err)
assert.NoError(t, f.DeleteSheet("XLSXSheet3")) assert.NoError(t, f.DeleteSheet("Sheet3"))
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetDeleteSheet.TestBook3.xlsx"))) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetDeleteSheet.TestBook3.xlsx")))
}) })
@ -933,7 +1015,7 @@ func TestSetDeleteSheet(t *testing.T) {
f, err := prepareTestBook4() f, err := prepareTestBook4()
assert.NoError(t, err) assert.NoError(t, err)
assert.NoError(t, f.DeleteSheet("Sheet1")) assert.NoError(t, f.DeleteSheet("Sheet1"))
assert.NoError(t, f.AddComment("Sheet1", Comment{Cell: "A1", Author: "Excelize", Runs: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment."}}})) assert.NoError(t, f.AddComment("Sheet1", Comment{Cell: "A1", Author: "Excelize", Paragraph: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment."}}}))
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetDeleteSheet.TestBook4.xlsx"))) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetDeleteSheet.TestBook4.xlsx")))
}) })
} }
@ -997,7 +1079,7 @@ func TestConditionalFormat(t *testing.T) {
f := NewFile() f := NewFile()
sheet1 := f.GetSheetName(0) sheet1 := f.GetSheetName(0)
fillCells(f, sheet1, 10, 15) assert.NoError(t, fillCells(f, sheet1, 10, 15))
var format1, format2, format3, format4 int var format1, format2, format3, format4 int
var err error var err error
@ -1051,7 +1133,7 @@ func TestConditionalFormat(t *testing.T) {
{ {
Type: "cell", Type: "cell",
Criteria: "between", Criteria: "between",
Format: format1, Format: &format1,
MinValue: "6", MinValue: "6",
MaxValue: "8", MaxValue: "8",
}, },
@ -1063,7 +1145,7 @@ func TestConditionalFormat(t *testing.T) {
{ {
Type: "cell", Type: "cell",
Criteria: ">", Criteria: ">",
Format: format3, Format: &format3,
Value: "6", Value: "6",
}, },
}, },
@ -1074,7 +1156,7 @@ func TestConditionalFormat(t *testing.T) {
{ {
Type: "top", Type: "top",
Criteria: "=", Criteria: "=",
Format: format3, Format: &format3,
}, },
}, },
)) ))
@ -1084,7 +1166,7 @@ func TestConditionalFormat(t *testing.T) {
{ {
Type: "unique", Type: "unique",
Criteria: "=", Criteria: "=",
Format: format2, Format: &format2,
}, },
}, },
)) ))
@ -1094,7 +1176,7 @@ func TestConditionalFormat(t *testing.T) {
{ {
Type: "duplicate", Type: "duplicate",
Criteria: "=", Criteria: "=",
Format: format2, Format: &format2,
}, },
}, },
)) ))
@ -1104,7 +1186,7 @@ func TestConditionalFormat(t *testing.T) {
{ {
Type: "top", Type: "top",
Criteria: "=", Criteria: "=",
Format: format1, Format: &format1,
Value: "6", Value: "6",
Percent: true, Percent: true,
}, },
@ -1116,7 +1198,7 @@ func TestConditionalFormat(t *testing.T) {
{ {
Type: "average", Type: "average",
Criteria: "=", Criteria: "=",
Format: format3, Format: &format3,
AboveAverage: true, AboveAverage: true,
}, },
}, },
@ -1127,7 +1209,7 @@ func TestConditionalFormat(t *testing.T) {
{ {
Type: "average", Type: "average",
Criteria: "=", Criteria: "=",
Format: format1, Format: &format1,
AboveAverage: false, AboveAverage: false,
}, },
}, },
@ -1150,7 +1232,7 @@ func TestConditionalFormat(t *testing.T) {
{ {
Type: "formula", Type: "formula",
Criteria: "L2<3", Criteria: "L2<3",
Format: format1, Format: &format1,
}, },
}, },
)) ))
@ -1160,21 +1242,23 @@ func TestConditionalFormat(t *testing.T) {
{ {
Type: "cell", Type: "cell",
Criteria: ">", Criteria: ">",
Format: format4, Format: &format4,
Value: "0", Value: "0",
}, },
}, },
)) ))
// Test set conditional format with invalid cell reference
assert.Equal(t, newCellNameToCoordinatesError("-", newInvalidCellNameError("-")), f.SetConditionalFormat("Sheet1", "A1:-", nil))
// Test set conditional format on not exists worksheet // Test set conditional format on not exists worksheet
assert.EqualError(t, f.SetConditionalFormat("SheetN", "L1:L10", nil), "sheet SheetN does not exist") assert.EqualError(t, f.SetConditionalFormat("SheetN", "L1:L10", nil), "sheet SheetN does not exist")
// Test set conditional format with invalid sheet name // Test set conditional format with invalid sheet name
assert.EqualError(t, f.SetConditionalFormat("Sheet:1", "L1:L10", nil), ErrSheetNameInvalid.Error()) assert.Equal(t, ErrSheetNameInvalid, f.SetConditionalFormat("Sheet:1", "L1:L10", nil))
err = f.SaveAs(filepath.Join("test", "TestConditionalFormat.xlsx")) err = f.SaveAs(filepath.Join("test", "TestConditionalFormat.xlsx"))
assert.NoError(t, err) assert.NoError(t, err)
// Set conditional format with illegal valid type // Set conditional format with illegal valid type
assert.NoError(t, f.SetConditionalFormat(sheet1, "K1:K10", assert.Equal(t, ErrParameterInvalid, f.SetConditionalFormat(sheet1, "K1:K10",
[]ConditionalFormatOptions{ []ConditionalFormatOptions{
{ {
Type: "", Type: "",
@ -1186,7 +1270,7 @@ func TestConditionalFormat(t *testing.T) {
}, },
)) ))
// Set conditional format with illegal criteria type // Set conditional format with illegal criteria type
assert.NoError(t, f.SetConditionalFormat(sheet1, "K1:K10", assert.Equal(t, ErrParameterInvalid, f.SetConditionalFormat(sheet1, "K1:K10",
[]ConditionalFormatOptions{ []ConditionalFormatOptions{
{ {
Type: "data_bar", Type: "data_bar",
@ -1200,7 +1284,7 @@ func TestConditionalFormat(t *testing.T) {
// Test create conditional format with invalid custom number format // Test create conditional format with invalid custom number format
var exp string var exp string
_, err = f.NewConditionalStyle(&Style{CustomNumFmt: &exp}) _, err = f.NewConditionalStyle(&Style{CustomNumFmt: &exp})
assert.EqualError(t, err, ErrCustomNumFmt.Error()) assert.Equal(t, ErrCustomNumFmt, err)
// Set conditional format with file without dxfs element should not return error // Set conditional format with file without dxfs element should not return error
f, err = OpenFile(filepath.Join("test", "Book1.xlsx")) f, err = OpenFile(filepath.Join("test", "Book1.xlsx"))
@ -1495,7 +1579,7 @@ func TestWorkSheetReader(t *testing.T) {
f = NewFile() f = NewFile()
f.Sheet.Delete("xl/worksheets/sheet1.xml") f.Sheet.Delete("xl/worksheets/sheet1.xml")
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData/></worksheet>`)) f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData/></worksheet>`))
f.checked = nil f.checked = sync.Map{}
_, err = f.workSheetReader("Sheet1") _, err = f.workSheetReader("Sheet1")
assert.NoError(t, err) assert.NoError(t, err)
} }
@ -1571,13 +1655,13 @@ func prepareTestBook1() (*File, error) {
func prepareTestBook3() (*File, error) { func prepareTestBook3() (*File, error) {
f := NewFile() f := NewFile()
if _, err := f.NewSheet("XLSXSheet2"); err != nil { if _, err := f.NewSheet("Sheet2"); err != nil {
return nil, err return nil, err
} }
if _, err := f.NewSheet("XLSXSheet3"); err != nil { if _, err := f.NewSheet("Sheet3"); err != nil {
return nil, err return nil, err
} }
if err := f.SetCellInt("XLSXSheet2", "A23", 56); err != nil { if err := f.SetCellInt("Sheet2", "A23", 56); err != nil {
return nil, err return nil, err
} }
if err := f.SetCellStr("Sheet1", "B20", "42"); err != nil { if err := f.SetCellStr("Sheet1", "B20", "42"); err != nil {
@ -1612,15 +1696,41 @@ func prepareTestBook4() (*File, error) {
return f, nil return f, nil
} }
func fillCells(f *File, sheet string, colCount, rowCount int) { func prepareTestBook5(opts Options) (*File, error) {
f := NewFile(opts)
var rowNum int
for _, idxRange := range [][]int{{27, 36}, {50, 81}} {
for numFmtIdx := idxRange[0]; numFmtIdx <= idxRange[1]; numFmtIdx++ {
rowNum++
styleID, err := f.NewStyle(&Style{NumFmt: numFmtIdx})
if err != nil {
return f, err
}
cell, err := CoordinatesToCellName(1, rowNum)
if err != nil {
return f, err
}
if err := f.SetCellValue("Sheet1", cell, 45162); err != nil {
return f, err
}
if err := f.SetCellStyle("Sheet1", cell, cell, styleID); err != nil {
return f, err
}
}
}
return f, nil
}
func fillCells(f *File, sheet string, colCount, rowCount int) error {
for col := 1; col <= colCount; col++ { for col := 1; col <= colCount; col++ {
for row := 1; row <= rowCount; row++ { for row := 1; row <= rowCount; row++ {
cell, _ := CoordinatesToCellName(col, row) cell, _ := CoordinatesToCellName(col, row)
if err := f.SetCellStr(sheet, cell, cell); err != nil { if err := f.SetCellStr(sheet, cell, cell); err != nil {
fmt.Println(err) return err
} }
} }
} }
return nil
} }
func BenchmarkOpenFile(b *testing.B) { func BenchmarkOpenFile(b *testing.B) {

54
file.go
View File

@ -1,4 +1,4 @@
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in // this source code is governed by a BSD-style license that can be found in
// the LICENSE file. // the LICENSE file.
// //
@ -7,7 +7,7 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming // Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of // API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.16 or later. // data. This library needs Go version 1.18 or later.
package excelize package excelize
@ -18,6 +18,7 @@ import (
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
"sort"
"strings" "strings"
"sync" "sync"
) )
@ -26,7 +27,7 @@ import (
// For example: // For example:
// //
// f := NewFile() // f := NewFile()
func NewFile() *File { func NewFile(opts ...Options) *File {
f := newFile() f := newFile()
f.Pkg.Store("_rels/.rels", []byte(xml.Header+templateRels)) f.Pkg.Store("_rels/.rels", []byte(xml.Header+templateRels))
f.Pkg.Store(defaultXMLPathDocPropsApp, []byte(xml.Header+templateDocpropsApp)) f.Pkg.Store(defaultXMLPathDocPropsApp, []byte(xml.Header+templateDocpropsApp))
@ -49,6 +50,7 @@ func NewFile() *File {
ws, _ := f.workSheetReader("Sheet1") ws, _ := f.workSheetReader("Sheet1")
f.Sheet.Store("xl/worksheets/sheet1.xml", ws) f.Sheet.Store("xl/worksheets/sheet1.xml", ws)
f.Theme, _ = f.themeReader() f.Theme, _ = f.themeReader()
f.options = f.getOptions(opts...)
return f return f
} }
@ -175,6 +177,7 @@ func (f *File) writeToZip(zw *zip.Writer) error {
f.commentsWriter() f.commentsWriter()
f.contentTypesWriter() f.contentTypesWriter()
f.drawingsWriter() f.drawingsWriter()
f.volatileDepsWriter()
f.vmlDrawingWriter() f.vmlDrawingWriter()
f.workBookWriter() f.workBookWriter()
f.workSheetWriter() f.workSheetWriter()
@ -190,43 +193,48 @@ func (f *File) writeToZip(zw *zip.Writer) error {
return err return err
} }
var from io.Reader var from io.Reader
from, err = stream.rawData.Reader() if from, err = stream.rawData.Reader(); err != nil {
if err != nil {
_ = stream.rawData.Close() _ = stream.rawData.Close()
return err return err
} }
_, err = io.Copy(fi, from) if _, err = io.Copy(fi, from); err != nil {
if err != nil {
return err return err
} }
} }
var err error var (
err error
files, tempFiles []string
)
f.Pkg.Range(func(path, content interface{}) bool { f.Pkg.Range(func(path, content interface{}) bool {
if err != nil {
return false
}
if _, ok := f.streams[path.(string)]; ok { if _, ok := f.streams[path.(string)]; ok {
return true return true
} }
var fi io.Writer files = append(files, path.(string))
fi, err = zw.Create(path.(string))
if err != nil {
return false
}
_, err = fi.Write(content.([]byte))
return true return true
}) })
sort.Sort(sort.Reverse(sort.StringSlice(files)))
for _, path := range files {
var fi io.Writer
if fi, err = zw.Create(path); err != nil {
break
}
content, _ := f.Pkg.Load(path)
_, err = fi.Write(content.([]byte))
}
f.tempFiles.Range(func(path, content interface{}) bool { f.tempFiles.Range(func(path, content interface{}) bool {
if _, ok := f.Pkg.Load(path); ok { if _, ok := f.Pkg.Load(path); ok {
return true return true
} }
var fi io.Writer tempFiles = append(tempFiles, path.(string))
fi, err = zw.Create(path.(string))
if err != nil {
return false
}
_, err = fi.Write(f.readBytes(path.(string)))
return true return true
}) })
sort.Sort(sort.Reverse(sort.StringSlice(tempFiles)))
for _, path := range tempFiles {
var fi io.Writer
if fi, err = zw.Create(path); err != nil {
break
}
_, err = fi.Write(f.readBytes(path))
}
return err return err
} }

25
go.mod
View File

@ -1,17 +1,22 @@
module github.com/xuri/excelize/v2 module github.com/xuri/excelize/v2
go 1.16 go 1.18
require ( require (
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826
github.com/richardlehane/mscfb v1.0.4 github.com/richardlehane/mscfb v1.0.4
github.com/stretchr/testify v1.8.0 github.com/stretchr/testify v1.9.0
github.com/xuri/efp v0.0.0-20220603152613-6918739fd470 github.com/tiendc/go-deepcopy v1.1.0
github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22 github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d
golang.org/x/crypto v0.8.0 github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7
golang.org/x/image v0.5.0 golang.org/x/crypto v0.29.0
golang.org/x/net v0.9.0 golang.org/x/image v0.18.0
golang.org/x/text v0.9.0 golang.org/x/net v0.31.0
golang.org/x/text v0.20.0
) )
require github.com/richardlehane/msoleps v1.0.3 // indirect require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/richardlehane/msoleps v1.0.4 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

73
go.sum
View File

@ -1,66 +1,29 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM= github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/richardlehane/msoleps v1.0.3 h1:aznSZzrwYRl3rLKRT3gUk9am7T/mLNSnJINvN0AQoVM= github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00=
github.com/richardlehane/msoleps v1.0.3/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/tiendc/go-deepcopy v1.1.0 h1:rBHhm5vg7WYnGLwktbQouodWjBXDoStOL4S7v/K8S4A=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/tiendc/go-deepcopy v1.1.0/go.mod h1:toXoeQoUqXOOS/X4sKuiAoSk6elIdqc0pN7MTgOOo2I=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7psK/lVsjIS2otl+1WyRyY=
github.com/xuri/efp v0.0.0-20220603152613-6918739fd470 h1:6932x8ltq1w4utjmfMPVj09jdMlkY0aiA6+Skbtl3/c= github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
github.com/xuri/efp v0.0.0-20220603152613-6918739fd470/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A=
github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22 h1:OAmKAfT06//esDdpi/DZ8Qsdt4+M5+ltca05dA5bG2M= github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ= golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI= golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4= golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

20
hsl.go
View File

@ -60,26 +60,26 @@ func hslModel(c color.Color) color.Color {
return HSL{h, s, l} return HSL{h, s, l}
} }
// RGBToHSL converts an RGB triple to a HSL triple. // RGBToHSL converts an RGB triple to an HSL triple.
func RGBToHSL(r, g, b uint8) (h, s, l float64) { func RGBToHSL(r, g, b uint8) (h, s, l float64) {
fR := float64(r) / 255 fR := float64(r) / 255
fG := float64(g) / 255 fG := float64(g) / 255
fB := float64(b) / 255 fB := float64(b) / 255
max := math.Max(math.Max(fR, fG), fB) maxVal := math.Max(math.Max(fR, fG), fB)
min := math.Min(math.Min(fR, fG), fB) minVal := math.Min(math.Min(fR, fG), fB)
l = (max + min) / 2 l = (maxVal + minVal) / 2
if max == min { if maxVal == minVal {
// Achromatic. // Achromatic.
h, s = 0, 0 h, s = 0, 0
} else { } else {
// Chromatic. // Chromatic.
d := max - min d := maxVal - minVal
if l > 0.5 { if l > 0.5 {
s = d / (2.0 - max - min) s = d / (2.0 - maxVal - minVal)
} else { } else {
s = d / (max + min) s = d / (maxVal + minVal)
} }
switch max { switch maxVal {
case fR: case fR:
h = (fG - fB) / d h = (fG - fB) / d
if fG < fB { if fG < fB {
@ -95,7 +95,7 @@ func RGBToHSL(r, g, b uint8) (h, s, l float64) {
return return
} }
// HSLToRGB converts an HSL triple to a RGB triple. // HSLToRGB converts an HSL triple to an RGB triple.
func HSLToRGB(h, s, l float64) (r, g, b uint8) { func HSLToRGB(h, s, l float64) (r, g, b uint8) {
var fR, fG, fB float64 var fR, fG, fB float64
if s == 0 { if s == 0 {

163
lib.go
View File

@ -1,4 +1,4 @@
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in // this source code is governed by a BSD-style license that can be found in
// the LICENSE file. // the LICENSE file.
// //
@ -7,7 +7,7 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming // Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of // API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.16 or later. // data. This library needs Go version 1.18 or later.
package excelize package excelize
@ -18,6 +18,7 @@ import (
"encoding/xml" "encoding/xml"
"fmt" "fmt"
"io" "io"
"math"
"math/big" "math/big"
"os" "os"
"regexp" "regexp"
@ -48,16 +49,22 @@ func (f *File) ReadZipReader(r *zip.Reader) (map[string][]byte, int, error) {
fileName = partName fileName = partName
} }
if strings.EqualFold(fileName, defaultXMLPathSharedStrings) && fileSize > f.options.UnzipXMLSizeLimit { if strings.EqualFold(fileName, defaultXMLPathSharedStrings) && fileSize > f.options.UnzipXMLSizeLimit {
if tempFile, err := f.unzipToTemp(v); err == nil { tempFile, err := f.unzipToTemp(v)
if tempFile != "" {
f.tempFiles.Store(fileName, tempFile) f.tempFiles.Store(fileName, tempFile)
}
if err == nil {
continue continue
} }
} }
if strings.HasPrefix(fileName, "xl/worksheets/sheet") { if strings.HasPrefix(strings.ToLower(fileName), "xl/worksheets/sheet") {
worksheets++ worksheets++
if fileSize > f.options.UnzipXMLSizeLimit && !v.FileInfo().IsDir() { if fileSize > f.options.UnzipXMLSizeLimit && !v.FileInfo().IsDir() {
if tempFile, err := f.unzipToTemp(v); err == nil { tempFile, err := f.unzipToTemp(v)
if tempFile != "" {
f.tempFiles.Store(fileName, tempFile) f.tempFiles.Store(fileName, tempFile)
}
if err == nil {
continue continue
} }
} }
@ -225,12 +232,18 @@ func ColumnNumberToName(num int) (string, error) {
if num < MinColumns || num > MaxColumns { if num < MinColumns || num > MaxColumns {
return "", ErrColumnNumber return "", ErrColumnNumber
} }
var col string estimatedLength := 0
for n := num; n > 0; n = (n - 1) / 26 {
estimatedLength++
}
result := make([]byte, estimatedLength)
for num > 0 { for num > 0 {
col = string(rune((num-1)%26+65)) + col estimatedLength--
result[estimatedLength] = byte((num-1)%26 + 'A')
num = (num - 1) / 26 num = (num - 1) / 26
} }
return col, nil return string(result), nil
} }
// CellNameToCoordinates converts alphanumeric cell name to [X, Y] coordinates // CellNameToCoordinates converts alphanumeric cell name to [X, Y] coordinates
@ -261,7 +274,10 @@ func CellNameToCoordinates(cell string) (int, int, error) {
// excelize.CoordinatesToCellName(1, 1, true) // returns "$A$1", nil // excelize.CoordinatesToCellName(1, 1, true) // returns "$A$1", nil
func CoordinatesToCellName(col, row int, abs ...bool) (string, error) { func CoordinatesToCellName(col, row int, abs ...bool) (string, error) {
if col < 1 || row < 1 { if col < 1 || row < 1 {
return "", fmt.Errorf("invalid cell reference [%d, %d]", col, row) return "", newCoordinatesToCellNameError(col, row)
}
if row > TotalRows {
return "", ErrMaxRows
} }
sign := "" sign := ""
for _, a := range abs { for _, a := range abs {
@ -313,7 +329,7 @@ func sortCoordinates(coordinates []int) error {
// coordinatesToRangeRef provides a function to convert a pair of coordinates // coordinatesToRangeRef provides a function to convert a pair of coordinates
// to range reference. // to range reference.
func (f *File) coordinatesToRangeRef(coordinates []int, abs ...bool) (string, error) { func coordinatesToRangeRef(coordinates []int, abs ...bool) (string, error) {
if len(coordinates) != 4 { if len(coordinates) != 4 {
return "", ErrCoordinates return "", ErrCoordinates
} }
@ -329,7 +345,7 @@ func (f *File) coordinatesToRangeRef(coordinates []int, abs ...bool) (string, er
} }
// getDefinedNameRefTo convert defined name to reference range. // getDefinedNameRefTo convert defined name to reference range.
func (f *File) getDefinedNameRefTo(definedNameName string, currentSheet string) (refTo string) { func (f *File) getDefinedNameRefTo(definedNameName, currentSheet string) (refTo string) {
var workbookRefTo, worksheetRefTo string var workbookRefTo, worksheetRefTo string
for _, definedName := range f.GetDefinedName() { for _, definedName := range f.GetDefinedName() {
if definedName.Name == definedNameName { if definedName.Name == definedNameName {
@ -350,7 +366,7 @@ func (f *File) getDefinedNameRefTo(definedNameName string, currentSheet string)
} }
// flatSqref convert reference sequence to cell reference list. // flatSqref convert reference sequence to cell reference list.
func (f *File) flatSqref(sqref string) (cells map[int][][]int, err error) { func flatSqref(sqref string) (cells map[int][][]int, err error) {
var coordinates []int var coordinates []int
cells = make(map[int][][]int) cells = make(map[int][][]int)
for _, ref := range strings.Fields(sqref) { for _, ref := range strings.Fields(sqref) {
@ -421,8 +437,8 @@ func boolPtr(b bool) *bool { return &b }
// intPtr returns a pointer to an int with the given value. // intPtr returns a pointer to an int with the given value.
func intPtr(i int) *int { return &i } func intPtr(i int) *int { return &i }
// uintPtr returns a pointer to an int with the given value. // uintPtr returns a pointer to an unsigned integer with the given value.
func uintPtr(i uint) *uint { return &i } func uintPtr(u uint) *uint { return &u }
// float64Ptr returns a pointer to a float64 with the given value. // float64Ptr returns a pointer to a float64 with the given value.
func float64Ptr(f float64) *float64 { return &f } func float64Ptr(f float64) *float64 { return &f }
@ -430,6 +446,30 @@ func float64Ptr(f float64) *float64 { return &f }
// stringPtr returns a pointer to a string with the given value. // stringPtr returns a pointer to a string with the given value.
func stringPtr(s string) *string { return &s } func stringPtr(s string) *string { return &s }
// Value extracts string data type text from a attribute value.
func (avb *attrValString) Value() string {
if avb != nil && avb.Val != nil {
return *avb.Val
}
return ""
}
// Value extracts boolean data type value from a attribute value.
func (avb *attrValBool) Value() bool {
if avb != nil && avb.Val != nil {
return *avb.Val
}
return false
}
// Value extracts float64 data type numeric from a attribute value.
func (attr *attrValFloat) Value() float64 {
if attr != nil && attr.Val != nil {
return *attr.Val
}
return 0
}
// MarshalXML convert the boolean data type to literal values 0 or 1 on // MarshalXML convert the boolean data type to literal values 0 or 1 on
// serialization. // serialization.
func (avb attrValBool) MarshalXML(e *xml.Encoder, start xml.StartElement) error { func (avb attrValBool) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
@ -493,6 +533,34 @@ func (avb *attrValBool) UnmarshalXML(d *xml.Decoder, start xml.StartElement) err
return nil return nil
} }
// MarshalXML encodes ext element with specified namespace attributes on
// serialization.
func (ext xlsxExt) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
start.Attr = ext.xmlns
return e.EncodeElement(decodeExt{URI: ext.URI, Content: ext.Content}, start)
}
// UnmarshalXML extracts ext element attributes namespace by giving XML decoder
// on deserialization.
func (ext *xlsxExt) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
for _, attr := range start.Attr {
if attr.Name.Local == "uri" {
continue
}
if attr.Name.Space == "xmlns" {
attr.Name.Space = ""
attr.Name.Local = "xmlns:" + attr.Name.Local
}
ext.xmlns = append(ext.xmlns, attr)
}
e := &decodeExt{}
if err := d.DecodeElement(&e, &start); err != nil {
return err
}
ext.URI, ext.Content = e.URI, e.Content
return nil
}
// namespaceStrictToTransitional provides a method to convert Strict and // namespaceStrictToTransitional provides a method to convert Strict and
// Transitional namespaces. // Transitional namespaces.
func namespaceStrictToTransitional(content []byte) []byte { func namespaceStrictToTransitional(content []byte) []byte {
@ -584,6 +652,16 @@ func getRootElement(d *xml.Decoder) []xml.Attr {
case xml.StartElement: case xml.StartElement:
tokenIdx++ tokenIdx++
if tokenIdx == 1 { if tokenIdx == 1 {
var ns bool
for i := 0; i < len(startElement.Attr); i++ {
if startElement.Attr[i].Value == NameSpaceSpreadSheet.Value &&
startElement.Attr[i].Name == NameSpaceSpreadSheet.Name {
ns = true
}
}
if !ns {
startElement.Attr = append(startElement.Attr, NameSpaceSpreadSheet)
}
return startElement.Attr return startElement.Attr
} }
} }
@ -623,8 +701,8 @@ func getXMLNamespace(space string, attr []xml.Attr) string {
func (f *File) replaceNameSpaceBytes(path string, contentMarshal []byte) []byte { func (f *File) replaceNameSpaceBytes(path string, contentMarshal []byte) []byte {
sourceXmlns := []byte(`xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">`) sourceXmlns := []byte(`xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">`)
targetXmlns := []byte(templateNamespaceIDMap) targetXmlns := []byte(templateNamespaceIDMap)
if attr, ok := f.xmlAttr[path]; ok { if attrs, ok := f.xmlAttr.Load(path); ok {
targetXmlns = []byte(genXMLNamespace(attr)) targetXmlns = []byte(genXMLNamespace(attrs.([]xml.Attr)))
} }
return bytesReplace(contentMarshal, sourceXmlns, bytes.ReplaceAll(targetXmlns, []byte(" mc:Ignorable=\"r\""), []byte{}), -1) return bytesReplace(contentMarshal, sourceXmlns, bytes.ReplaceAll(targetXmlns, []byte(" mc:Ignorable=\"r\""), []byte{}), -1)
} }
@ -635,29 +713,36 @@ func (f *File) addNameSpaces(path string, ns xml.Attr) {
exist := false exist := false
mc := false mc := false
ignore := -1 ignore := -1
if attr, ok := f.xmlAttr[path]; ok { if attrs, ok := f.xmlAttr.Load(path); ok {
for i, attribute := range attr { for i, attr := range attrs.([]xml.Attr) {
if attribute.Name.Local == ns.Name.Local && attribute.Name.Space == ns.Name.Space { if attr.Name.Local == ns.Name.Local && attr.Name.Space == ns.Name.Space {
exist = true exist = true
} }
if attribute.Name.Local == "Ignorable" && getXMLNamespace(attribute.Name.Space, attr) == "mc" { if attr.Name.Local == "Ignorable" && getXMLNamespace(attr.Name.Space, attrs.([]xml.Attr)) == "mc" {
ignore = i ignore = i
} }
if attribute.Name.Local == "mc" && attribute.Name.Space == "xmlns" { if attr.Name.Local == "mc" && attr.Name.Space == "xmlns" {
mc = true mc = true
} }
} }
} }
if !exist { if !exist {
f.xmlAttr[path] = append(f.xmlAttr[path], ns) attrs, _ := f.xmlAttr.Load(path)
if attrs == nil {
attrs = []xml.Attr{}
}
attrs = append(attrs.([]xml.Attr), ns)
f.xmlAttr.Store(path, attrs)
if !mc { if !mc {
f.xmlAttr[path] = append(f.xmlAttr[path], SourceRelationshipCompatibility) attrs = append(attrs.([]xml.Attr), SourceRelationshipCompatibility)
f.xmlAttr.Store(path, attrs)
} }
if ignore == -1 { if ignore == -1 {
f.xmlAttr[path] = append(f.xmlAttr[path], xml.Attr{ attrs = append(attrs.([]xml.Attr), xml.Attr{
Name: xml.Name{Local: "Ignorable", Space: "mc"}, Name: xml.Name{Local: "Ignorable", Space: "mc"},
Value: ns.Name.Local, Value: ns.Name.Local,
}) })
f.xmlAttr.Store(path, attrs)
return return
} }
f.setIgnorableNameSpace(path, ignore, ns) f.setIgnorableNameSpace(path, ignore, ns)
@ -668,8 +753,10 @@ func (f *File) addNameSpaces(path string, ns xml.Attr) {
// by the given attribute. // by the given attribute.
func (f *File) setIgnorableNameSpace(path string, index int, ns xml.Attr) { func (f *File) setIgnorableNameSpace(path string, index int, ns xml.Attr) {
ignorableNS := []string{"c14", "cdr14", "a14", "pic14", "x14", "xdr14", "x14ac", "dsp", "mso14", "dgm14", "x15", "x12ac", "x15ac", "xr", "xr2", "xr3", "xr4", "xr5", "xr6", "xr7", "xr8", "xr9", "xr10", "xr11", "xr12", "xr13", "xr14", "xr15", "x15", "x16", "x16r2", "mo", "mx", "mv", "o", "v"} ignorableNS := []string{"c14", "cdr14", "a14", "pic14", "x14", "xdr14", "x14ac", "dsp", "mso14", "dgm14", "x15", "x12ac", "x15ac", "xr", "xr2", "xr3", "xr4", "xr5", "xr6", "xr7", "xr8", "xr9", "xr10", "xr11", "xr12", "xr13", "xr14", "xr15", "x15", "x16", "x16r2", "mo", "mx", "mv", "o", "v"}
if inStrSlice(strings.Fields(f.xmlAttr[path][index].Value), ns.Name.Local, true) == -1 && inStrSlice(ignorableNS, ns.Name.Local, true) != -1 { xmlAttrs, _ := f.xmlAttr.Load(path)
f.xmlAttr[path][index].Value = strings.TrimSpace(fmt.Sprintf("%s %s", f.xmlAttr[path][index].Value, ns.Name.Local)) if inStrSlice(strings.Fields(xmlAttrs.([]xml.Attr)[index].Value), ns.Name.Local, true) == -1 && inStrSlice(ignorableNS, ns.Name.Local, true) != -1 {
xmlAttrs.([]xml.Attr)[index].Value = strings.TrimSpace(fmt.Sprintf("%s %s", xmlAttrs.([]xml.Attr)[index].Value, ns.Name.Local))
f.xmlAttr.Store(path, xmlAttrs)
} }
} }
@ -767,6 +854,30 @@ func bstrMarshal(s string) (result string) {
return result return result
} }
// newRat converts decimals to rational fractions with the required precision.
func newRat(n float64, iterations int64, prec float64) *big.Rat {
x := int64(math.Floor(n))
y := n - float64(x)
rat := continuedFraction(y, 1, iterations, prec)
return rat.Add(rat, new(big.Rat).SetInt64(x))
}
// continuedFraction returns rational from decimal with the continued fraction
// algorithm.
func continuedFraction(n float64, i int64, limit int64, prec float64) *big.Rat {
if i >= limit || n <= prec {
return big.NewRat(0, 1)
}
inverted := 1 / n
y := int64(math.Floor(inverted))
x := inverted - float64(y)
ratY := new(big.Rat).SetInt64(y)
ratNext := continuedFraction(x, i+1, limit, prec)
res := ratY.Add(ratY, ratNext)
res = res.Inv(res)
return res
}
// Stack defined an abstract data type that serves as a collection of elements. // Stack defined an abstract data type that serves as a collection of elements.
type Stack struct { type Stack struct {
list *list.List list *list.List

View File

@ -218,14 +218,13 @@ func TestCoordinatesToCellName_Error(t *testing.T) {
} }
func TestCoordinatesToRangeRef(t *testing.T) { func TestCoordinatesToRangeRef(t *testing.T) {
f := NewFile() _, err := coordinatesToRangeRef([]int{})
_, err := f.coordinatesToRangeRef([]int{})
assert.EqualError(t, err, ErrCoordinates.Error()) assert.EqualError(t, err, ErrCoordinates.Error())
_, err = f.coordinatesToRangeRef([]int{1, -1, 1, 1}) _, err = coordinatesToRangeRef([]int{1, -1, 1, 1})
assert.EqualError(t, err, "invalid cell reference [1, -1]") assert.Equal(t, newCoordinatesToCellNameError(1, -1), err)
_, err = f.coordinatesToRangeRef([]int{1, 1, 1, -1}) _, err = coordinatesToRangeRef([]int{1, 1, 1, -1})
assert.EqualError(t, err, "invalid cell reference [1, -1]") assert.Equal(t, newCoordinatesToCellNameError(1, -1), err)
ref, err := f.coordinatesToRangeRef([]int{1, 1, 1, 1}) ref, err := coordinatesToRangeRef([]int{1, 1, 1, 1})
assert.NoError(t, err) assert.NoError(t, err)
assert.EqualValues(t, ref, "A1:A1") assert.EqualValues(t, ref, "A1:A1")
} }
@ -238,6 +237,12 @@ func TestInStrSlice(t *testing.T) {
assert.EqualValues(t, -1, inStrSlice([]string{}, "", true)) assert.EqualValues(t, -1, inStrSlice([]string{}, "", true))
} }
func TestAttrValue(t *testing.T) {
assert.Empty(t, (&attrValString{}).Value())
assert.False(t, (&attrValBool{}).Value())
assert.Zero(t, (&attrValFloat{}).Value())
}
func TestBoolValMarshal(t *testing.T) { func TestBoolValMarshal(t *testing.T) {
bold := true bold := true
node := &xlsxFont{B: &attrValBool{Val: &bold}} node := &xlsxFont{B: &attrValBool{Val: &bold}}
@ -268,6 +273,15 @@ func TestBoolValUnmarshalXML(t *testing.T) {
assert.EqualError(t, attr.UnmarshalXML(xml.NewDecoder(strings.NewReader("")), xml.StartElement{}), io.EOF.Error()) assert.EqualError(t, attr.UnmarshalXML(xml.NewDecoder(strings.NewReader("")), xml.StartElement{}), io.EOF.Error())
} }
func TestExtUnmarshalXML(t *testing.T) {
f, extLst := NewFile(), decodeExtLst{}
expected := fmt.Sprintf(`<extLst><ext uri="%s" xmlns:x14="%s"/></extLst>`,
ExtURISlicerCachesX14, NameSpaceSpreadSheetX14.Value)
assert.NoError(t, f.xmlNewDecoder(strings.NewReader(expected)).Decode(&extLst))
assert.Len(t, extLst.Ext, 1)
assert.Equal(t, extLst.Ext[0].URI, ExtURISlicerCachesX14)
}
func TestBytesReplace(t *testing.T) { func TestBytesReplace(t *testing.T) {
s := []byte{0x01} s := []byte{0x01}
assert.EqualValues(t, s, bytesReplace(s, []byte{}, []byte{}, 0)) assert.EqualValues(t, s, bytesReplace(s, []byte{}, []byte{}, 0))
@ -275,13 +289,19 @@ func TestBytesReplace(t *testing.T) {
func TestGetRootElement(t *testing.T) { func TestGetRootElement(t *testing.T) {
assert.Len(t, getRootElement(xml.NewDecoder(strings.NewReader(""))), 0) assert.Len(t, getRootElement(xml.NewDecoder(strings.NewReader(""))), 0)
// Test get workbook root element which all workbook XML namespace has prefix
f := NewFile()
d := f.xmlNewDecoder(bytes.NewReader([]byte(`<x:workbook xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:x="http://schemas.openxmlformats.org/spreadsheetml/2006/main"></x:workbook>`)))
assert.Len(t, getRootElement(d), 3)
} }
func TestSetIgnorableNameSpace(t *testing.T) { func TestSetIgnorableNameSpace(t *testing.T) {
f := NewFile() f := NewFile()
f.xmlAttr["xml_path"] = []xml.Attr{{}} f.xmlAttr.Store("xml_path", []xml.Attr{{}})
f.setIgnorableNameSpace("xml_path", 0, xml.Attr{Name: xml.Name{Local: "c14"}}) f.setIgnorableNameSpace("xml_path", 0, xml.Attr{Name: xml.Name{Local: "c14"}})
assert.EqualValues(t, "c14", f.xmlAttr["xml_path"][0].Value) attrs, ok := f.xmlAttr.Load("xml_path")
assert.EqualValues(t, "c14", attrs.([]xml.Attr)[0].Value)
assert.True(t, ok)
} }
func TestStack(t *testing.T) { func TestStack(t *testing.T) {
@ -342,10 +362,8 @@ func TestReadBytes(t *testing.T) {
} }
func TestUnzipToTemp(t *testing.T) { func TestUnzipToTemp(t *testing.T) {
for _, v := range []string{"go1.19", "go1.20"} { if ver := runtime.Version(); strings.HasPrefix(ver, "go1.19") || strings.HasPrefix(ver, "go1.2") {
if strings.HasPrefix(runtime.Version(), v) { t.Skip()
t.Skip()
}
} }
os.Setenv("TMPDIR", "test") os.Setenv("TMPDIR", "test")
defer os.Unsetenv("TMPDIR") defer os.Unsetenv("TMPDIR")

View File

@ -1,4 +1,4 @@
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in // this source code is governed by a BSD-style license that can be found in
// the LICENSE file. // the LICENSE file.
// //
@ -7,7 +7,7 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming // Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of // API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.16 or later. // data. This library needs Go version 1.18 or later.
package excelize package excelize
@ -49,24 +49,35 @@ func (mc *xlsxMergeCell) Rect() ([]int, error) {
// | | // | |
// |A8(x3,y4) C8(x4,y4)| // |A8(x3,y4) C8(x4,y4)|
// +------------------------+ // +------------------------+
func (f *File) MergeCell(sheet, hCell, vCell string) error { func (f *File) MergeCell(sheet, topLeftCell, bottomRightCell string) error {
rect, err := rangeRefToCoordinates(hCell + ":" + vCell) rect, err := rangeRefToCoordinates(topLeftCell + ":" + bottomRightCell)
if err != nil { if err != nil {
return err return err
} }
// Correct the range reference, such correct C1:B3 to B1:C3. // Correct the range reference, such correct C1:B3 to B1:C3.
_ = sortCoordinates(rect) _ = sortCoordinates(rect)
hCell, _ = CoordinatesToCellName(rect[0], rect[1]) topLeftCell, _ = CoordinatesToCellName(rect[0], rect[1])
vCell, _ = CoordinatesToCellName(rect[2], rect[3]) bottomRightCell, _ = CoordinatesToCellName(rect[2], rect[3])
ws, err := f.workSheetReader(sheet) ws, err := f.workSheetReader(sheet)
if err != nil { if err != nil {
return err return err
} }
ws.Lock() ws.mu.Lock()
defer ws.Unlock() defer ws.mu.Unlock()
ref := hCell + ":" + vCell for col := rect[0]; col <= rect[2]; col++ {
for row := rect[1]; row <= rect[3]; row++ {
if col == rect[0] && row == rect[1] {
continue
}
ws.prepareSheetXML(col, row)
c := &ws.SheetData.Row[row-1].C[col-1]
c.setCellDefault("")
_ = f.removeFormula(c, ws, sheet)
}
}
ref := topLeftCell + ":" + bottomRightCell
if ws.MergeCells != nil { if ws.MergeCells != nil {
ws.MergeCells.Cells = append(ws.MergeCells.Cells, &xlsxMergeCell{Ref: ref, rect: rect}) ws.MergeCells.Cells = append(ws.MergeCells.Cells, &xlsxMergeCell{Ref: ref, rect: rect})
} else { } else {
@ -82,14 +93,14 @@ func (f *File) MergeCell(sheet, hCell, vCell string) error {
// err := f.UnmergeCell("Sheet1", "D3", "E9") // err := f.UnmergeCell("Sheet1", "D3", "E9")
// //
// Attention: overlapped range will also be unmerged. // Attention: overlapped range will also be unmerged.
func (f *File) UnmergeCell(sheet, hCell, vCell string) error { func (f *File) UnmergeCell(sheet, topLeftCell, bottomRightCell string) error {
ws, err := f.workSheetReader(sheet) ws, err := f.workSheetReader(sheet)
if err != nil { if err != nil {
return err return err
} }
ws.Lock() ws.mu.Lock()
defer ws.Unlock() defer ws.mu.Unlock()
rect1, err := rangeRefToCoordinates(hCell + ":" + vCell) rect1, err := rangeRefToCoordinates(topLeftCell + ":" + bottomRightCell)
if err != nil { if err != nil {
return err return err
} }
@ -128,8 +139,8 @@ func (f *File) UnmergeCell(sheet, hCell, vCell string) error {
return nil return nil
} }
// GetMergeCells provides a function to get all merged cells from a worksheet // GetMergeCells provides a function to get all merged cells from a specific
// currently. // worksheet.
func (f *File) GetMergeCells(sheet string) ([]MergeCell, error) { func (f *File) GetMergeCells(sheet string) ([]MergeCell, error) {
var mergeCells []MergeCell var mergeCells []MergeCell
ws, err := f.workSheetReader(sheet) ws, err := f.workSheetReader(sheet)
@ -265,9 +276,9 @@ func mergeCell(cell1, cell2 *xlsxMergeCell) *xlsxMergeCell {
if rect1[3] < rect2[3] { if rect1[3] < rect2[3] {
rect1[3], rect2[3] = rect2[3], rect1[3] rect1[3], rect2[3] = rect2[3], rect1[3]
} }
hCell, _ := CoordinatesToCellName(rect1[0], rect1[1]) topLeftCell, _ := CoordinatesToCellName(rect1[0], rect1[1])
vCell, _ := CoordinatesToCellName(rect1[2], rect1[3]) bottomRightCell, _ := CoordinatesToCellName(rect1[2], rect1[3])
return &xlsxMergeCell{rect: rect1, Ref: hCell + ":" + vCell} return &xlsxMergeCell{rect: rect1, Ref: topLeftCell + ":" + bottomRightCell}
} }
// MergeCell define a merged cell data. // MergeCell define a merged cell data.
@ -289,5 +300,9 @@ func (m *MergeCell) GetStartAxis() string {
// GetEndAxis returns the bottom right cell reference of merged range, for // GetEndAxis returns the bottom right cell reference of merged range, for
// example: "D4". // example: "D4".
func (m *MergeCell) GetEndAxis() string { func (m *MergeCell) GetEndAxis() string {
return strings.Split((*m)[0], ":")[1] coordinates := strings.Split((*m)[0], ":")
if len(coordinates) == 2 {
return coordinates[1]
}
return coordinates[0]
} }

View File

@ -13,14 +13,18 @@ func TestMergeCell(t *testing.T) {
t.FailNow() t.FailNow()
} }
assert.EqualError(t, f.MergeCell("Sheet1", "A", "B"), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.EqualError(t, f.MergeCell("Sheet1", "A", "B"), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
assert.NoError(t, f.MergeCell("Sheet1", "D9", "D9")) for _, cells := range [][]string{
assert.NoError(t, f.MergeCell("Sheet1", "D9", "E9")) {"D9", "D9"},
assert.NoError(t, f.MergeCell("Sheet1", "H14", "G13")) {"D9", "E9"},
assert.NoError(t, f.MergeCell("Sheet1", "C9", "D8")) {"H14", "G13"},
assert.NoError(t, f.MergeCell("Sheet1", "F11", "G13")) {"C9", "D8"},
assert.NoError(t, f.MergeCell("Sheet1", "H7", "B15")) {"F11", "G13"},
assert.NoError(t, f.MergeCell("Sheet1", "D11", "F13")) {"H7", "B15"},
assert.NoError(t, f.MergeCell("Sheet1", "G10", "K12")) {"D11", "F13"},
{"G10", "K12"},
} {
assert.NoError(t, f.MergeCell("Sheet1", cells[0], cells[1]))
}
assert.NoError(t, f.SetCellValue("Sheet1", "G11", "set value in merged cell")) assert.NoError(t, f.SetCellValue("Sheet1", "G11", "set value in merged cell"))
assert.NoError(t, f.SetCellInt("Sheet1", "H11", 100)) assert.NoError(t, f.SetCellInt("Sheet1", "H11", 100))
assert.NoError(t, f.SetCellValue("Sheet1", "I11", 0.5)) assert.NoError(t, f.SetCellValue("Sheet1", "I11", 0.5))
@ -39,32 +43,29 @@ func TestMergeCell(t *testing.T) {
_, err = f.NewSheet("Sheet3") _, err = f.NewSheet("Sheet3")
assert.NoError(t, err) assert.NoError(t, err)
assert.NoError(t, f.MergeCell("Sheet3", "D11", "F13"))
assert.NoError(t, f.MergeCell("Sheet3", "G10", "K12"))
assert.NoError(t, f.MergeCell("Sheet3", "B1", "D5")) // B1:D5 for _, cells := range [][]string{
assert.NoError(t, f.MergeCell("Sheet3", "E1", "F5")) // E1:F5 {"D11", "F13"},
{"G10", "K12"},
assert.NoError(t, f.MergeCell("Sheet3", "H2", "I5")) {"B1", "D5"}, // B1:D5
assert.NoError(t, f.MergeCell("Sheet3", "I4", "J6")) // H2:J6 {"E1", "F5"}, // E1:F5
{"H2", "I5"},
assert.NoError(t, f.MergeCell("Sheet3", "M2", "N5")) {"I4", "J6"}, // H2:J6
assert.NoError(t, f.MergeCell("Sheet3", "L4", "M6")) // L2:N6 {"M2", "N5"},
{"L4", "M6"}, // L2:N6
assert.NoError(t, f.MergeCell("Sheet3", "P4", "Q7")) {"P4", "Q7"},
assert.NoError(t, f.MergeCell("Sheet3", "O2", "P5")) // O2:Q7 {"O2", "P5"}, // O2:Q7
{"A9", "B12"},
assert.NoError(t, f.MergeCell("Sheet3", "A9", "B12")) {"B7", "C9"}, // A7:C12
assert.NoError(t, f.MergeCell("Sheet3", "B7", "C9")) // A7:C12 {"E9", "F10"},
{"D8", "G12"},
assert.NoError(t, f.MergeCell("Sheet3", "E9", "F10")) {"I8", "I12"},
assert.NoError(t, f.MergeCell("Sheet3", "D8", "G12")) {"I10", "K10"},
{"M8", "Q13"},
assert.NoError(t, f.MergeCell("Sheet3", "I8", "I12")) {"N10", "O11"},
assert.NoError(t, f.MergeCell("Sheet3", "I10", "K10")) } {
assert.NoError(t, f.MergeCell("Sheet3", cells[0], cells[1]))
assert.NoError(t, f.MergeCell("Sheet3", "M8", "Q13")) }
assert.NoError(t, f.MergeCell("Sheet3", "N10", "O11"))
// Test merge cells on not exists worksheet // Test merge cells on not exists worksheet
assert.EqualError(t, f.MergeCell("SheetN", "N10", "O11"), "sheet SheetN does not exist") assert.EqualError(t, f.MergeCell("SheetN", "N10", "O11"), "sheet SheetN does not exist")
@ -79,6 +80,13 @@ func TestMergeCell(t *testing.T) {
assert.True(t, ok) assert.True(t, ok)
ws.(*xlsxWorksheet).MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{nil, nil}} ws.(*xlsxWorksheet).MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{nil, nil}}
assert.NoError(t, f.MergeCell("Sheet1", "A2", "B3")) assert.NoError(t, f.MergeCell("Sheet1", "A2", "B3"))
// Test getting merged cells with the same start and end axis
ws.(*xlsxWorksheet).MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A1"}}}
mergedCells, err := f.GetMergeCells("Sheet1")
assert.NoError(t, err)
assert.Equal(t, "A1", mergedCells[0].GetStartAxis())
assert.Equal(t, "A1", mergedCells[0].GetEndAxis())
assert.Empty(t, mergedCells[0].GetCellValue())
} }
func TestMergeCellOverlap(t *testing.T) { func TestMergeCellOverlap(t *testing.T) {
@ -207,7 +215,7 @@ func TestFlatMergedCells(t *testing.T) {
} }
func TestMergeCellsParser(t *testing.T) { func TestMergeCellsParser(t *testing.T) {
f := NewFile() ws := &xlsxWorksheet{MergeCells: &xlsxMergeCells{Cells: []*xlsxMergeCell{nil}}}
_, err := f.mergeCellsParser(&xlsxWorksheet{MergeCells: &xlsxMergeCells{Cells: []*xlsxMergeCell{nil}}}, "A1") _, err := ws.mergeCellsParser("A1")
assert.NoError(t, err) assert.NoError(t, err)
} }

6925
numfmt.go

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in // this source code is governed by a BSD-style license that can be found in
// the LICENSE file. // the LICENSE file.
// //
@ -7,7 +7,7 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming // Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of // API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.16 or later. // data. This library needs Go version 1.18 or later.
package excelize package excelize
@ -23,6 +23,18 @@ import (
"strings" "strings"
) )
// PictureInsertType defines the type of the picture has been inserted into the
// worksheet.
type PictureInsertType int
// Insert picture types.
const (
PictureInsertTypePlaceOverCells PictureInsertType = iota
PictureInsertTypePlaceInCell
PictureInsertTypeIMAGE
PictureInsertTypeDISPIMG
)
// parseGraphicOptions provides a function to parse the format settings of // parseGraphicOptions provides a function to parse the format settings of
// the picture with default value. // the picture with default value.
func parseGraphicOptions(opts *GraphicOptions) *GraphicOptions { func parseGraphicOptions(opts *GraphicOptions) *GraphicOptions {
@ -30,8 +42,8 @@ func parseGraphicOptions(opts *GraphicOptions) *GraphicOptions {
return &GraphicOptions{ return &GraphicOptions{
PrintObject: boolPtr(true), PrintObject: boolPtr(true),
Locked: boolPtr(true), Locked: boolPtr(true),
ScaleX: defaultPictureScale, ScaleX: defaultDrawingScale,
ScaleY: defaultPictureScale, ScaleY: defaultDrawingScale,
} }
} }
if opts.PrintObject == nil { if opts.PrintObject == nil {
@ -41,10 +53,10 @@ func parseGraphicOptions(opts *GraphicOptions) *GraphicOptions {
opts.Locked = boolPtr(true) opts.Locked = boolPtr(true)
} }
if opts.ScaleX == 0 { if opts.ScaleX == 0 {
opts.ScaleX = defaultPictureScale opts.ScaleX = defaultDrawingScale
} }
if opts.ScaleY == 0 { if opts.ScaleY == 0 {
opts.ScaleY = defaultPictureScale opts.ScaleY = defaultDrawingScale
} }
return opts return opts
} }
@ -52,7 +64,10 @@ func parseGraphicOptions(opts *GraphicOptions) *GraphicOptions {
// AddPicture provides the method to add picture in a sheet by given picture // AddPicture provides the method to add picture in a sheet by given picture
// format set (such as offset, scale, aspect ratio setting and print settings) // format set (such as offset, scale, aspect ratio setting and print settings)
// and file path, supported image types: BMP, EMF, EMZ, GIF, JPEG, JPG, PNG, // and file path, supported image types: BMP, EMF, EMZ, GIF, JPEG, JPG, PNG,
// SVG, TIF, TIFF, WMF, and WMZ. This function is concurrency safe. For example: // SVG, TIF, TIFF, WMF, and WMZ. This function is concurrency-safe. Note that
// this function only supports adding pictures placed over the cells currently,
// and doesn't support adding pictures placed in cells or creating the Kingsoft
// WPS Office embedded image cells. For example:
// //
// package main // package main
// //
@ -110,41 +125,50 @@ func parseGraphicOptions(opts *GraphicOptions) *GraphicOptions {
// } // }
// } // }
// //
// The optional parameter "AutoFit" specifies if you make image size auto-fits the // The optional parameter "AltText" is used to add alternative text to a graph
// cell, the default value of that is 'false'. // object.
// //
// The optional parameter "Hyperlink" specifies the hyperlink of the image. // The optional parameter "PrintObject" indicates whether the graph object is
// printed when the worksheet is printed, the default value of that is 'true'.
//
// The optional parameter "Locked" indicates whether lock the graph object.
// Locking an object has no effect unless the sheet is protected.
//
// The optional parameter "LockAspectRatio" indicates whether lock aspect ratio
// for the graph object, the default value of that is 'false'.
//
// The optional parameter "AutoFit" specifies if you make graph object size
// auto-fits the cell, the default value of that is 'false'.
//
// The optional parameter "AutoFitIgnoreAspect" specifies if fill the cell with
// the image and ignore its aspect ratio, the default value of that is 'false'.
// This option only works when the "AutoFit" is enabled.
//
// The optional parameter "OffsetX" specifies the horizontal offset of the graph
// object with the cell, the default value of that is 0.
//
// The optional parameter "OffsetY" specifies the vertical offset of the graph
// object with the cell, the default value of that is 0.
//
// The optional parameter "ScaleX" specifies the horizontal scale of graph
// object, the default value of that is 1.0 which presents 100%.
//
// The optional parameter "ScaleY" specifies the vertical scale of graph object,
// the default value of that is 1.0 which presents 100%.
//
// The optional parameter "Hyperlink" specifies the hyperlink of the graph
// object.
// //
// The optional parameter "HyperlinkType" defines two types of // The optional parameter "HyperlinkType" defines two types of
// hyperlink "External" for website or "Location" for moving to one of the // hyperlink "External" for website or "Location" for moving to one of the
// cells in this workbook. When the "HyperlinkType" is "Location", // cells in this workbook. When the "HyperlinkType" is "Location",
// coordinates need to start with "#". // coordinates need to start with "#".
// //
// The optional parameter "Positioning" defines two types of the position of an // The optional parameter "Positioning" defines 3 types of the position of a
// image in an Excel spreadsheet, "oneCell" (Move but don't size with // graph object in a spreadsheet: "oneCell" (Move but don't size with
// cells) or "absolute" (Don't move or size with cells). If you don't set this // cells), "twoCell" (Move and size with cells), and "absolute" (Don't move or
// parameter, the default positioning is move and size with cells. // size with cells). If you don't set this parameter, the default positioning
// // is to move and size with cells.
// The optional parameter "PrintObject" indicates whether the image is printed
// when the worksheet is printed, the default value of that is 'true'.
//
// The optional parameter "LockAspectRatio" indicates whether lock aspect
// ratio for the image, the default value of that is 'false'.
//
// The optional parameter "Locked" indicates whether lock the image. Locking
// an object has no effect unless the sheet is protected.
//
// The optional parameter "OffsetX" specifies the horizontal offset of the
// image with the cell, the default value of that is 0.
//
// The optional parameter "ScaleX" specifies the horizontal scale of images,
// the default value of that is 1.0 which presents 100%.
//
// The optional parameter "OffsetY" specifies the vertical offset of the
// image with the cell, the default value of that is 0.
//
// The optional parameter "ScaleY" specifies the vertical scale of images,
// the default value of that is 1.0 which presents 100%.
func (f *File) AddPicture(sheet, cell, name string, opts *GraphicOptions) error { func (f *File) AddPicture(sheet, cell, name string, opts *GraphicOptions) error {
var err error var err error
// Check picture exists first. // Check picture exists first.
@ -162,8 +186,10 @@ func (f *File) AddPicture(sheet, cell, name string, opts *GraphicOptions) error
// AddPictureFromBytes provides the method to add picture in a sheet by given // AddPictureFromBytes provides the method to add picture in a sheet by given
// picture format set (such as offset, scale, aspect ratio setting and print // picture format set (such as offset, scale, aspect ratio setting and print
// settings), file base name, extension name and file bytes, supported image // settings), file base name, extension name and file bytes, supported image
// types: EMF, EMZ, GIF, JPEG, JPG, PNG, SVG, TIF, TIFF, WMF, and WMZ. For // types: EMF, EMZ, GIF, JPEG, JPG, PNG, SVG, TIF, TIFF, WMF, and WMZ. Note that
// example: // this function only supports adding pictures placed over the cells currently,
// and doesn't support adding pictures placed in cells or creating the Kingsoft
// WPS Office embedded image cells. For example:
// //
// package main // package main
// //
@ -206,24 +232,41 @@ func (f *File) AddPictureFromBytes(sheet, cell string, pic *Picture) error {
if !ok { if !ok {
return ErrImgExt return ErrImgExt
} }
if pic.InsertType != PictureInsertTypePlaceOverCells {
return ErrParameterInvalid
}
options := parseGraphicOptions(pic.Format) options := parseGraphicOptions(pic.Format)
img, _, err := image.DecodeConfig(bytes.NewReader(pic.File)) img, _, err := image.DecodeConfig(bytes.NewReader(pic.File))
if err != nil { if err != nil {
return err return err
} }
// Read sheet data. // Read sheet data
f.mu.Lock()
ws, err := f.workSheetReader(sheet) ws, err := f.workSheetReader(sheet)
if err != nil { if err != nil {
f.mu.Unlock()
return err return err
} }
ws.Lock() f.mu.Unlock()
ws.mu.Lock()
// Add first picture for given sheet, create xl/drawings/ and xl/drawings/_rels/ folder. // Add first picture for given sheet, create xl/drawings/ and xl/drawings/_rels/ folder.
drawingID := f.countDrawings() + 1 drawingID := f.countDrawings() + 1
drawingXML := "xl/drawings/drawing" + strconv.Itoa(drawingID) + ".xml" drawingXML := "xl/drawings/drawing" + strconv.Itoa(drawingID) + ".xml"
drawingID, drawingXML = f.prepareDrawing(ws, drawingID, sheet, drawingXML) drawingID, drawingXML = f.prepareDrawing(ws, drawingID, sheet, drawingXML)
drawingRels := "xl/drawings/_rels/drawing" + strconv.Itoa(drawingID) + ".xml.rels" drawingRels := "xl/drawings/_rels/drawing" + strconv.Itoa(drawingID) + ".xml.rels"
mediaStr := ".." + strings.TrimPrefix(f.addMedia(pic.File, ext), "xl") mediaStr := ".." + strings.TrimPrefix(f.addMedia(pic.File, ext), "xl")
drawingRID := f.addRels(drawingRels, SourceRelationshipImage, mediaStr, hyperlinkType) var drawingRID int
if rels, _ := f.relsReader(drawingRels); rels != nil {
for _, rel := range rels.Relationships {
if rel.Type == SourceRelationshipImage && rel.Target == mediaStr {
drawingRID, _ = strconv.Atoi(strings.TrimPrefix(rel.ID, "rId"))
break
}
}
}
if drawingRID == 0 {
drawingRID = f.addRels(drawingRels, SourceRelationshipImage, mediaStr, hyperlinkType)
}
// Add picture with hyperlink. // Add picture with hyperlink.
if options.Hyperlink != "" && options.HyperlinkType != "" { if options.Hyperlink != "" && options.HyperlinkType != "" {
if options.HyperlinkType == "External" { if options.HyperlinkType == "External" {
@ -231,7 +274,7 @@ func (f *File) AddPictureFromBytes(sheet, cell string, pic *Picture) error {
} }
drawingHyperlinkRID = f.addRels(drawingRels, SourceRelationshipHyperLink, options.Hyperlink, hyperlinkType) drawingHyperlinkRID = f.addRels(drawingRels, SourceRelationshipHyperLink, options.Hyperlink, hyperlinkType)
} }
ws.Unlock() ws.mu.Unlock()
err = f.addDrawingPicture(sheet, drawingXML, cell, ext, drawingRID, drawingHyperlinkRID, img, options) err = f.addDrawingPicture(sheet, drawingXML, cell, ext, drawingRID, drawingHyperlinkRID, img, options)
if err != nil { if err != nil {
return err return err
@ -243,29 +286,6 @@ func (f *File) AddPictureFromBytes(sheet, cell string, pic *Picture) error {
return err return err
} }
// deleteSheetRelationships provides a function to delete relationships in
// xl/worksheets/_rels/sheet%d.xml.rels by given worksheet name and
// relationship index.
func (f *File) deleteSheetRelationships(sheet, rID string) {
name, ok := f.getSheetXMLPath(sheet)
if !ok {
name = strings.ToLower(sheet) + ".xml"
}
rels := "xl/worksheets/_rels/" + strings.TrimPrefix(name, "xl/worksheets/") + ".rels"
sheetRels, _ := f.relsReader(rels)
if sheetRels == nil {
sheetRels = &xlsxRelationships{}
}
sheetRels.Lock()
defer sheetRels.Unlock()
for k, v := range sheetRels.Relationships {
if v.ID == rID {
sheetRels.Relationships = append(sheetRels.Relationships[:k], sheetRels.Relationships[k+1:]...)
}
}
f.Relationships.Store(rels, sheetRels)
}
// addSheetLegacyDrawing provides a function to add legacy drawing element to // addSheetLegacyDrawing provides a function to add legacy drawing element to
// xl/worksheets/sheet%d.xml by given worksheet name and relationship index. // xl/worksheets/sheet%d.xml by given worksheet name and relationship index.
func (f *File) addSheetLegacyDrawing(sheet string, rID int) { func (f *File) addSheetLegacyDrawing(sheet string, rID int) {
@ -275,6 +295,16 @@ func (f *File) addSheetLegacyDrawing(sheet string, rID int) {
} }
} }
// addSheetLegacyDrawingHF provides a function to add legacy drawing
// header/footer element to xl/worksheets/sheet%d.xml by given
// worksheet name and relationship index.
func (f *File) addSheetLegacyDrawingHF(sheet string, rID int) {
ws, _ := f.workSheetReader(sheet)
ws.LegacyDrawingHF = &xlsxLegacyDrawingHF{
RID: "rId" + strconv.Itoa(rID),
}
}
// addSheetDrawing provides a function to add drawing element to // addSheetDrawing provides a function to add drawing element to
// xl/worksheets/sheet%d.xml by given worksheet name and relationship index. // xl/worksheets/sheet%d.xml by given worksheet name and relationship index.
func (f *File) addSheetDrawing(sheet string, rID int) { func (f *File) addSheetDrawing(sheet string, rID int) {
@ -300,23 +330,20 @@ func (f *File) addSheetPicture(sheet string, rID int) error {
// countDrawings provides a function to get drawing files count storage in the // countDrawings provides a function to get drawing files count storage in the
// folder xl/drawings. // folder xl/drawings.
func (f *File) countDrawings() int { func (f *File) countDrawings() int {
var c1, c2 int drawings := map[string]struct{}{}
f.Pkg.Range(func(k, v interface{}) bool { f.Pkg.Range(func(k, v interface{}) bool {
if strings.Contains(k.(string), "xl/drawings/drawing") { if strings.Contains(k.(string), "xl/drawings/drawing") {
c1++ drawings[k.(string)] = struct{}{}
} }
return true return true
}) })
f.Drawings.Range(func(rel, value interface{}) bool { f.Drawings.Range(func(rel, value interface{}) bool {
if strings.Contains(rel.(string), "xl/drawings/drawing") { if strings.Contains(rel.(string), "xl/drawings/drawing") {
c2++ drawings[rel.(string)] = struct{}{}
} }
return true return true
}) })
if c1 < c2 { return len(drawings)
return c2
}
return c1
} }
// addDrawingPicture provides a function to add picture by given sheet, // addDrawingPicture provides a function to add picture by given sheet,
@ -327,18 +354,18 @@ func (f *File) addDrawingPicture(sheet, drawingXML, cell, ext string, rID, hyper
if err != nil { if err != nil {
return err return err
} }
if opts.Positioning != "" && inStrSlice(supportedPositioning, opts.Positioning, true) == -1 {
return ErrParameterInvalid
}
width, height := img.Width, img.Height width, height := img.Width, img.Height
if opts.AutoFit { if opts.AutoFit {
width, height, col, row, err = f.drawingResize(sheet, cell, float64(width), float64(height), opts) if width, height, col, row, err = f.drawingResize(sheet, cell, float64(width), float64(height), opts); err != nil {
if err != nil {
return err return err
} }
} else { } else {
width = int(float64(width) * opts.ScaleX) width = int(float64(width) * opts.ScaleX)
height = int(float64(height) * opts.ScaleY) height = int(float64(height) * opts.ScaleY)
} }
col--
row--
colStart, rowStart, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(sheet, col, row, opts.OffsetX, opts.OffsetY, width, height) colStart, rowStart, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(sheet, col, row, opts.OffsetX, opts.OffsetY, width, height)
content, cNvPrID, err := f.drawingParser(drawingXML) content, cNvPrID, err := f.drawingParser(drawingXML)
if err != nil { if err != nil {
@ -391,8 +418,8 @@ func (f *File) addDrawingPicture(sheet, drawingXML, cell, ext string, rID, hyper
FLocksWithSheet: *opts.Locked, FLocksWithSheet: *opts.Locked,
FPrintsWithSheet: *opts.PrintObject, FPrintsWithSheet: *opts.PrintObject,
} }
content.Lock() content.mu.Lock()
defer content.Unlock() defer content.mu.Unlock()
content.TwoCellAnchor = append(content.TwoCellAnchor, &twoCellAnchor) content.TwoCellAnchor = append(content.TwoCellAnchor, &twoCellAnchor)
f.Drawings.Store(drawingXML, content) f.Drawings.Store(drawingXML, content)
return err return err
@ -435,130 +462,6 @@ func (f *File) addMedia(file []byte, ext string) string {
return media return media
} }
// setContentTypePartImageExtensions provides a function to set the content
// type for relationship parts and the Main Document part.
func (f *File) setContentTypePartImageExtensions() error {
imageTypes := map[string]string{
"bmp": "image/", "jpeg": "image/", "png": "image/", "gif": "image/",
"svg": "image/", "tiff": "image/", "emf": "image/x-", "wmf": "image/x-",
"emz": "image/x-", "wmz": "image/x-",
}
content, err := f.contentTypesReader()
if err != nil {
return err
}
content.Lock()
defer content.Unlock()
for _, file := range content.Defaults {
delete(imageTypes, file.Extension)
}
for extension, prefix := range imageTypes {
content.Defaults = append(content.Defaults, xlsxDefault{
Extension: extension,
ContentType: prefix + extension,
})
}
return err
}
// setContentTypePartVMLExtensions provides a function to set the content type
// for relationship parts and the Main Document part.
func (f *File) setContentTypePartVMLExtensions() error {
var vml bool
content, err := f.contentTypesReader()
if err != nil {
return err
}
content.Lock()
defer content.Unlock()
for _, v := range content.Defaults {
if v.Extension == "vml" {
vml = true
}
}
if !vml {
content.Defaults = append(content.Defaults, xlsxDefault{
Extension: "vml",
ContentType: ContentTypeVML,
})
}
return err
}
// addContentTypePart provides a function to add content type part
// relationships in the file [Content_Types].xml by given index.
func (f *File) addContentTypePart(index int, contentType string) error {
setContentType := map[string]func() error{
"comments": f.setContentTypePartVMLExtensions,
"drawings": f.setContentTypePartImageExtensions,
}
partNames := map[string]string{
"chart": "/xl/charts/chart" + strconv.Itoa(index) + ".xml",
"chartsheet": "/xl/chartsheets/sheet" + strconv.Itoa(index) + ".xml",
"comments": "/xl/comments" + strconv.Itoa(index) + ".xml",
"drawings": "/xl/drawings/drawing" + strconv.Itoa(index) + ".xml",
"table": "/xl/tables/table" + strconv.Itoa(index) + ".xml",
"pivotTable": "/xl/pivotTables/pivotTable" + strconv.Itoa(index) + ".xml",
"pivotCache": "/xl/pivotCache/pivotCacheDefinition" + strconv.Itoa(index) + ".xml",
"sharedStrings": "/xl/sharedStrings.xml",
}
contentTypes := map[string]string{
"chart": ContentTypeDrawingML,
"chartsheet": ContentTypeSpreadSheetMLChartsheet,
"comments": ContentTypeSpreadSheetMLComments,
"drawings": ContentTypeDrawing,
"table": ContentTypeSpreadSheetMLTable,
"pivotTable": ContentTypeSpreadSheetMLPivotTable,
"pivotCache": ContentTypeSpreadSheetMLPivotCacheDefinition,
"sharedStrings": ContentTypeSpreadSheetMLSharedStrings,
}
s, ok := setContentType[contentType]
if ok {
if err := s(); err != nil {
return err
}
}
content, err := f.contentTypesReader()
if err != nil {
return err
}
content.Lock()
defer content.Unlock()
for _, v := range content.Overrides {
if v.PartName == partNames[contentType] {
return err
}
}
content.Overrides = append(content.Overrides, xlsxOverride{
PartName: partNames[contentType],
ContentType: contentTypes[contentType],
})
return err
}
// getSheetRelationshipsTargetByID provides a function to get Target attribute
// value in xl/worksheets/_rels/sheet%d.xml.rels by given worksheet name and
// relationship index.
func (f *File) getSheetRelationshipsTargetByID(sheet, rID string) string {
name, ok := f.getSheetXMLPath(sheet)
if !ok {
name = strings.ToLower(sheet) + ".xml"
}
rels := "xl/worksheets/_rels/" + strings.TrimPrefix(name, "xl/worksheets/") + ".rels"
sheetRels, _ := f.relsReader(rels)
if sheetRels == nil {
sheetRels = &xlsxRelationships{}
}
sheetRels.Lock()
defer sheetRels.Unlock()
for _, v := range sheetRels.Relationships {
if v.ID == rID {
return v.Target
}
}
return ""
}
// GetPictures provides a function to get picture meta info and raw content // GetPictures provides a function to get picture meta info and raw content
// embed in spreadsheet by given worksheet and cell name. This function // embed in spreadsheet by given worksheet and cell name. This function
// returns the image contents as []byte data types. This function is // returns the image contents as []byte data types. This function is
@ -591,24 +494,62 @@ func (f *File) GetPictures(sheet, cell string) ([]Picture, error) {
} }
col-- col--
row-- row--
f.mu.Lock()
ws, err := f.workSheetReader(sheet) ws, err := f.workSheetReader(sheet)
if err != nil {
f.mu.Unlock()
return nil, err
}
f.mu.Unlock()
if ws.Drawing == nil {
return f.getCellImages(sheet, cell)
}
target := f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID)
drawingXML := strings.TrimPrefix(strings.ReplaceAll(target, "..", "xl"), "/")
drawingRelationships := strings.ReplaceAll(
strings.ReplaceAll(drawingXML, "xl/drawings", "xl/drawings/_rels"), ".xml", ".xml.rels")
imgs, err := f.getCellImages(sheet, cell)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if ws.Drawing == nil { pics, err := f.getPicture(row, col, drawingXML, drawingRelationships)
if err != nil {
return nil, err return nil, err
} }
target := f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID) return append(imgs, pics...), err
drawingXML := strings.ReplaceAll(target, "..", "xl") }
drawingRelationships := strings.ReplaceAll(
strings.ReplaceAll(target, "../drawings", "xl/drawings/_rels"), ".xml", ".xml.rels")
return f.getPicture(row, col, drawingXML, drawingRelationships) // GetPictureCells returns all picture cell references in a worksheet by a
// specific worksheet name.
func (f *File) GetPictureCells(sheet string) ([]string, error) {
f.mu.Lock()
ws, err := f.workSheetReader(sheet)
if err != nil {
f.mu.Unlock()
return nil, err
}
f.mu.Unlock()
if ws.Drawing == nil {
return f.getImageCells(sheet)
}
target := f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID)
drawingXML := strings.TrimPrefix(strings.ReplaceAll(target, "..", "xl"), "/")
drawingRelationships := strings.ReplaceAll(
strings.ReplaceAll(drawingXML, "xl/drawings", "xl/drawings/_rels"), ".xml", ".xml.rels")
embeddedImageCells, err := f.getImageCells(sheet)
if err != nil {
return nil, err
}
imageCells, err := f.getPictureCells(drawingXML, drawingRelationships)
if err != nil {
return nil, err
}
return append(embeddedImageCells, imageCells...), err
} }
// DeletePicture provides a function to delete all pictures in a cell by given // DeletePicture provides a function to delete all pictures in a cell by given
// worksheet name and cell reference. Note that the image file won't be deleted // worksheet name and cell reference.
// from the document currently.
func (f *File) DeletePicture(sheet, cell string) error { func (f *File) DeletePicture(sheet, cell string) error {
col, row, err := CellNameToCoordinates(cell) col, row, err := CellNameToCoordinates(cell)
if err != nil { if err != nil {
@ -624,85 +565,127 @@ func (f *File) DeletePicture(sheet, cell string) error {
return err return err
} }
drawingXML := strings.ReplaceAll(f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID), "..", "xl") drawingXML := strings.ReplaceAll(f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID), "..", "xl")
return f.deleteDrawing(col, row, drawingXML, "Pic") drawingRels := "xl/drawings/_rels/" + filepath.Base(drawingXML) + ".rels"
rID, err := f.deleteDrawing(col, row, drawingXML, "Pic")
if err != nil {
return err
}
rels := f.getDrawingRelationships(drawingRels, rID)
if rels == nil {
return err
}
var used bool
checkPicRef := func(k, v interface{}) bool {
if strings.Contains(k.(string), "xl/drawings/_rels/drawing") {
r, err := f.relsReader(k.(string))
if err != nil {
return true
}
for _, rel := range r.Relationships {
if rel.ID != rels.ID && rel.Type == SourceRelationshipImage &&
filepath.Base(rel.Target) == filepath.Base(rels.Target) {
used = true
}
}
}
return true
}
f.Relationships.Range(checkPicRef)
f.Pkg.Range(checkPicRef)
if !used {
f.Pkg.Delete(strings.Replace(rels.Target, "../", "xl/", -1))
}
f.deleteDrawingRels(drawingRels, rID)
return err
} }
// getPicture provides a function to get picture base name and raw content // getPicture provides a function to get picture base name and raw content
// embed in spreadsheet by given coordinates and drawing relationships. // embed in spreadsheet by given coordinates and drawing relationships.
func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) (pics []Picture, err error) { func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) (pics []Picture, err error) {
var ( var wsDr *xlsxWsDr
wsDr *xlsxWsDr
ok bool
deWsDr *decodeWsDr
drawRel *xlsxRelationship
deTwoCellAnchor *decodeTwoCellAnchor
)
if wsDr, _, err = f.drawingParser(drawingXML); err != nil { if wsDr, _, err = f.drawingParser(drawingXML); err != nil {
return return
} }
if pics = f.getPicturesFromWsDr(row, col, drawingRelationships, wsDr); len(pics) > 0 { wsDr.mu.Lock()
return defer wsDr.mu.Unlock()
} cond := func(from *xlsxFrom) bool { return from.Col == col && from.Row == row }
deWsDr = new(decodeWsDr) cond2 := func(from *decodeFrom) bool { return from.Col == col && from.Row == row }
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(drawingXML)))). cb := func(a *xdrCellAnchor, r *xlsxRelationship) {
Decode(deWsDr); err != nil && err != io.EOF { pic := Picture{Extension: filepath.Ext(r.Target), Format: &GraphicOptions{}, InsertType: PictureInsertTypePlaceOverCells}
return if buffer, _ := f.Pkg.Load(filepath.ToSlash(filepath.Clean("xl/drawings/" + r.Target))); buffer != nil {
} pic.File = buffer.([]byte)
err = nil pic.Format.AltText = a.Pic.NvPicPr.CNvPr.Descr
for _, anchor := range deWsDr.TwoCellAnchor { pics = append(pics, pic)
deTwoCellAnchor = new(decodeTwoCellAnchor)
if err = f.xmlNewDecoder(strings.NewReader("<decodeTwoCellAnchor>" + anchor.Content + "</decodeTwoCellAnchor>")).
Decode(deTwoCellAnchor); err != nil && err != io.EOF {
return
} }
if err = nil; deTwoCellAnchor.From != nil && deTwoCellAnchor.Pic != nil { }
if deTwoCellAnchor.From.Col == col && deTwoCellAnchor.From.Row == row { cb2 := func(a *decodeCellAnchor, r *xlsxRelationship) {
drawRel = f.getDrawingRelationships(drawingRelationships, deTwoCellAnchor.Pic.BlipFill.Blip.Embed) var target string
if _, ok = supportedImageTypes[strings.ToLower(filepath.Ext(drawRel.Target))]; ok { if strings.HasPrefix(r.Target, "/") {
pic := Picture{Extension: filepath.Ext(drawRel.Target), Format: &GraphicOptions{}} target = strings.TrimPrefix(r.Target, "/")
if buffer, _ := f.Pkg.Load(strings.ReplaceAll(drawRel.Target, "..", "xl")); buffer != nil { } else {
pic.File = buffer.([]byte) target = filepath.ToSlash(filepath.Clean("xl/drawings/" + r.Target))
pic.Format.AltText = deTwoCellAnchor.Pic.NvPicPr.CNvPr.Descr
pics = append(pics, pic)
}
return
}
}
} }
pic := Picture{Extension: filepath.Ext(target), Format: &GraphicOptions{}, InsertType: PictureInsertTypePlaceOverCells}
if buffer, _ := f.Pkg.Load(target); buffer != nil {
pic.File = buffer.([]byte)
pic.Format.AltText = a.Pic.NvPicPr.CNvPr.Descr
pics = append(pics, pic)
}
}
for _, anchor := range wsDr.TwoCellAnchor {
f.extractCellAnchor(anchor, drawingRelationships, cond, cb, cond2, cb2)
}
for _, anchor := range wsDr.OneCellAnchor {
f.extractCellAnchor(anchor, drawingRelationships, cond, cb, cond2, cb2)
} }
return return
} }
// getPicturesFromWsDr provides a function to get picture base name and raw // extractCellAnchor extract drawing object from cell anchor by giving drawing
// content in worksheet drawing by given coordinates and drawing // cell anchor, drawing relationships part path, conditional and callback
// relationships. // function.
func (f *File) getPicturesFromWsDr(row, col int, drawingRelationships string, wsDr *xlsxWsDr) (pics []Picture) { func (f *File) extractCellAnchor(anchor *xdrCellAnchor, drawingRelationships string,
var ( cond func(from *xlsxFrom) bool, cb func(anchor *xdrCellAnchor, rels *xlsxRelationship),
ok bool cond2 func(from *decodeFrom) bool, cb2 func(anchor *decodeCellAnchor, rels *xlsxRelationship),
anchor *xdrCellAnchor ) {
drawRel *xlsxRelationship var drawRel *xlsxRelationship
) if anchor.GraphicFrame == "" {
wsDr.Lock()
defer wsDr.Unlock()
for _, anchor = range wsDr.TwoCellAnchor {
if anchor.From != nil && anchor.Pic != nil { if anchor.From != nil && anchor.Pic != nil {
if anchor.From.Col == col && anchor.From.Row == row { if cond(anchor.From) {
if drawRel = f.getDrawingRelationships(drawingRelationships, if drawRel = f.getDrawingRelationships(drawingRelationships,
anchor.Pic.BlipFill.Blip.Embed); drawRel != nil { anchor.Pic.BlipFill.Blip.Embed); drawRel != nil {
if _, ok = supportedImageTypes[strings.ToLower(filepath.Ext(drawRel.Target))]; ok { if _, ok := supportedImageTypes[strings.ToLower(filepath.Ext(drawRel.Target))]; ok {
pic := Picture{Extension: filepath.Ext(drawRel.Target), Format: &GraphicOptions{}} cb(anchor, drawRel)
if buffer, _ := f.Pkg.Load(strings.ReplaceAll(drawRel.Target, "..", "xl")); buffer != nil {
pic.File = buffer.([]byte)
pic.Format.AltText = anchor.Pic.NvPicPr.CNvPr.Descr
pics = append(pics, pic)
}
} }
} }
} }
} }
return
}
f.extractDecodeCellAnchor(anchor, drawingRelationships, cond2, cb2)
}
// extractDecodeCellAnchor extract drawing object from cell anchor by giving
// decoded drawing cell anchor, drawing relationships part path, conditional and
// callback function.
func (f *File) extractDecodeCellAnchor(anchor *xdrCellAnchor, drawingRelationships string,
cond func(from *decodeFrom) bool, cb func(anchor *decodeCellAnchor, rels *xlsxRelationship),
) {
var (
drawRel *xlsxRelationship
deCellAnchor = new(decodeCellAnchor)
)
_ = f.xmlNewDecoder(strings.NewReader("<decodeCellAnchor>" + anchor.GraphicFrame + "</decodeCellAnchor>")).Decode(&deCellAnchor)
if deCellAnchor.From != nil && deCellAnchor.Pic != nil {
if cond(deCellAnchor.From) {
if drawRel = f.getDrawingRelationships(drawingRelationships, deCellAnchor.Pic.BlipFill.Blip.Embed); drawRel != nil {
if _, ok := supportedImageTypes[strings.ToLower(filepath.Ext(drawRel.Target))]; ok {
cb(deCellAnchor, drawRel)
}
}
}
} }
return
} }
// getDrawingRelationships provides a function to get drawing relationships // getDrawingRelationships provides a function to get drawing relationships
@ -710,8 +693,8 @@ func (f *File) getPicturesFromWsDr(row, col int, drawingRelationships string, ws
// relationship ID. // relationship ID.
func (f *File) getDrawingRelationships(rels, rID string) *xlsxRelationship { func (f *File) getDrawingRelationships(rels, rID string) *xlsxRelationship {
if drawingRels, _ := f.relsReader(rels); drawingRels != nil { if drawingRels, _ := f.relsReader(rels); drawingRels != nil {
drawingRels.Lock() drawingRels.mu.Lock()
defer drawingRels.Unlock() defer drawingRels.mu.Unlock()
for _, v := range drawingRels.Relationships { for _, v := range drawingRels.Relationships {
if v.ID == rID { if v.ID == rID {
return &v return &v
@ -750,10 +733,7 @@ func (f *File) drawingResize(sheet, cell string, width, height float64, opts *Gr
if inMergeCell { if inMergeCell {
continue continue
} }
if inMergeCell, err = f.checkCellInRangeRef(cell, mergeCell[0]); err != nil { if inMergeCell, err = f.checkCellInRangeRef(cell, mergeCell[0]); err == nil {
return
}
if inMergeCell {
rng, _ = cellRefsToCoordinates(mergeCell.GetStartAxis(), mergeCell.GetEndAxis()) rng, _ = cellRefsToCoordinates(mergeCell.GetStartAxis(), mergeCell.GetEndAxis())
_ = sortCoordinates(rng) _ = sortCoordinates(rng)
} }
@ -776,7 +756,244 @@ func (f *File) drawingResize(sheet, cell string, width, height float64, opts *Gr
asp := float64(cellHeight) / height asp := float64(cellHeight) / height
height, width = float64(cellHeight), width*asp height, width = float64(cellHeight), width*asp
} }
if opts.AutoFitIgnoreAspect {
width, height = float64(cellWidth), float64(cellHeight)
}
width, height = width-float64(opts.OffsetX), height-float64(opts.OffsetY) width, height = width-float64(opts.OffsetX), height-float64(opts.OffsetY)
w, h = int(width*opts.ScaleX), int(height*opts.ScaleY) w, h = int(width*opts.ScaleX), int(height*opts.ScaleY)
return return
} }
// getPictureCells provides a function to get all picture cell references in a
// worksheet by given drawing part path and drawing relationships path.
func (f *File) getPictureCells(drawingXML, drawingRelationships string) ([]string, error) {
var (
cells []string
err error
wsDr *xlsxWsDr
)
if wsDr, _, err = f.drawingParser(drawingXML); err != nil {
return cells, err
}
wsDr.mu.Lock()
defer wsDr.mu.Unlock()
cond := func(from *xlsxFrom) bool { return true }
cond2 := func(from *decodeFrom) bool { return true }
cb := func(a *xdrCellAnchor, r *xlsxRelationship) {
if _, ok := f.Pkg.Load(filepath.ToSlash(filepath.Clean("xl/drawings/" + r.Target))); ok {
if cell, err := CoordinatesToCellName(a.From.Col+1, a.From.Row+1); err == nil && inStrSlice(cells, cell, true) == -1 {
cells = append(cells, cell)
}
}
}
cb2 := func(a *decodeCellAnchor, r *xlsxRelationship) {
var target string
if strings.HasPrefix(r.Target, "/") {
target = strings.TrimPrefix(r.Target, "/")
} else {
target = filepath.ToSlash(filepath.Clean("xl/drawings/" + r.Target))
}
if _, ok := f.Pkg.Load(target); ok {
if cell, err := CoordinatesToCellName(a.From.Col+1, a.From.Row+1); err == nil && inStrSlice(cells, cell, true) == -1 {
cells = append(cells, cell)
}
}
}
for _, anchor := range wsDr.TwoCellAnchor {
f.extractCellAnchor(anchor, drawingRelationships, cond, cb, cond2, cb2)
}
for _, anchor := range wsDr.OneCellAnchor {
f.extractCellAnchor(anchor, drawingRelationships, cond, cb, cond2, cb2)
}
return cells, err
}
// cellImagesReader provides a function to get the pointer to the structure
// after deserialization of xl/cellimages.xml.
func (f *File) cellImagesReader() (*decodeCellImages, error) {
if f.DecodeCellImages == nil {
f.DecodeCellImages = new(decodeCellImages)
if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathCellImages)))).
Decode(f.DecodeCellImages); err != nil && err != io.EOF {
return f.DecodeCellImages, err
}
}
return f.DecodeCellImages, nil
}
// getImageCells returns all the cell images and the Kingsoft WPS
// Office embedded image cells reference by given worksheet name.
func (f *File) getImageCells(sheet string) ([]string, error) {
var (
err error
cells []string
)
ws, err := f.workSheetReader(sheet)
if err != nil {
return cells, err
}
for _, row := range ws.SheetData.Row {
for _, c := range row.C {
if c.F != nil && c.F.Content != "" &&
strings.HasPrefix(strings.TrimPrefix(strings.TrimPrefix(c.F.Content, "="), "_xlfn."), "DISPIMG") {
if _, err = f.CalcCellValue(sheet, c.R); err != nil {
return cells, err
}
cells = append(cells, c.R)
}
r, err := f.getImageCellRel(&c, &Picture{})
if err != nil {
return cells, err
}
if r != nil {
cells = append(cells, c.R)
}
}
}
return cells, err
}
// getRichDataRichValueRel returns relationship of the cell image by given meta
// blocks value.
func (f *File) getRichDataRichValueRel(val string) (*xlsxRelationship, error) {
var r *xlsxRelationship
idx, err := strconv.Atoi(val)
if err != nil {
return r, err
}
richValueRel, err := f.richValueRelReader()
if err != nil {
return r, err
}
if idx >= len(richValueRel.Rels) {
return r, err
}
rID := richValueRel.Rels[idx].ID
if r = f.getRichDataRichValueRelRelationships(rID); r != nil && r.Type != SourceRelationshipImage {
return nil, err
}
return r, err
}
// getRichDataWebImagesRel returns relationship of a web image by given meta
// blocks value.
func (f *File) getRichDataWebImagesRel(val string) (*xlsxRelationship, error) {
var r *xlsxRelationship
idx, err := strconv.Atoi(val)
if err != nil {
return r, err
}
richValueWebImages, err := f.richValueWebImageReader()
if err != nil {
return r, err
}
if idx >= len(richValueWebImages.WebImageSrd) {
return r, err
}
rID := richValueWebImages.WebImageSrd[idx].Blip.RID
if r = f.getRichValueWebImageRelationships(rID); r != nil && r.Type != SourceRelationshipImage {
return nil, err
}
return r, err
}
// getImageCellRel returns the cell image relationship.
func (f *File) getImageCellRel(c *xlsxC, pic *Picture) (*xlsxRelationship, error) {
var r *xlsxRelationship
if c.Vm == nil || c.V != formulaErrorVALUE {
return r, nil
}
metaData, err := f.metadataReader()
if err != nil {
return r, err
}
vmd := metaData.ValueMetadata
if vmd == nil || int(*c.Vm) > len(vmd.Bk) || len(vmd.Bk[*c.Vm-1].Rc) == 0 {
return r, err
}
richValueIdx := vmd.Bk[*c.Vm-1].Rc[0].V
richValue, err := f.richValueReader()
if err != nil {
return r, err
}
if richValueIdx >= len(richValue.Rv) {
return r, err
}
rv := richValue.Rv[richValueIdx].V
if len(rv) == 2 && rv[1] == "5" {
pic.InsertType = PictureInsertTypePlaceInCell
return f.getRichDataRichValueRel(rv[0])
}
// cell image inserted by IMAGE formula function
if len(rv) > 3 && rv[1]+rv[2] == "10" {
pic.InsertType = PictureInsertTypeIMAGE
return f.getRichDataWebImagesRel(rv[0])
}
return r, err
}
// getCellImages provides a function to get the cell images and
// the Kingsoft WPS Office embedded cell images by given worksheet name and cell
// reference.
func (f *File) getCellImages(sheet, cell string) ([]Picture, error) {
pics, err := f.getDispImages(sheet, cell)
if err != nil {
return pics, err
}
_, err = f.getCellStringFunc(sheet, cell, func(x *xlsxWorksheet, c *xlsxC) (string, bool, error) {
pic := Picture{Format: &GraphicOptions{}, InsertType: PictureInsertTypePlaceInCell}
r, err := f.getImageCellRel(c, &pic)
if err != nil || r == nil {
return "", true, err
}
pic.Extension = filepath.Ext(r.Target)
if buffer, _ := f.Pkg.Load(strings.TrimPrefix(strings.ReplaceAll(r.Target, "..", "xl"), "/")); buffer != nil {
pic.File = buffer.([]byte)
pics = append(pics, pic)
}
return "", true, nil
})
return pics, err
}
// getDispImages provides a function to get the Kingsoft WPS Office embedded
// cell images by given worksheet name and cell reference.
func (f *File) getDispImages(sheet, cell string) ([]Picture, error) {
formula, err := f.GetCellFormula(sheet, cell)
if err != nil {
return nil, err
}
if !strings.HasPrefix(strings.TrimPrefix(strings.TrimPrefix(formula, "="), "_xlfn."), "DISPIMG") {
return nil, err
}
imgID, err := f.CalcCellValue(sheet, cell)
if err != nil {
return nil, err
}
cellImages, err := f.cellImagesReader()
if err != nil {
return nil, err
}
rels, err := f.relsReader(defaultXMLPathCellImagesRels)
if rels == nil {
return nil, err
}
var pics []Picture
for _, cellImg := range cellImages.CellImage {
if cellImg.Pic.NvPicPr.CNvPr.Name == imgID {
for _, r := range rels.Relationships {
if r.ID == cellImg.Pic.BlipFill.Blip.Embed {
pic := Picture{Extension: filepath.Ext(r.Target), Format: &GraphicOptions{}, InsertType: PictureInsertTypeDISPIMG}
if buffer, _ := f.Pkg.Load("xl/" + r.Target); buffer != nil {
pic.File = buffer.([]byte)
pic.Format.AltText = cellImg.Pic.NvPicPr.CNvPr.Descr
pics = append(pics, pic)
}
}
}
}
}
return pics, err
}

View File

@ -12,10 +12,9 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/stretchr/testify/assert"
_ "golang.org/x/image/bmp" _ "golang.org/x/image/bmp"
_ "golang.org/x/image/tiff" _ "golang.org/x/image/tiff"
"github.com/stretchr/testify/assert"
) )
func BenchmarkAddPictureFromBytes(b *testing.B) { func BenchmarkAddPictureFromBytes(b *testing.B) {
@ -49,6 +48,7 @@ func TestAddPicture(t *testing.T) {
// Test add picture to worksheet with autofit // Test add picture to worksheet with autofit
assert.NoError(t, f.AddPicture("Sheet1", "A30", filepath.Join("test", "images", "excel.jpg"), &GraphicOptions{AutoFit: true})) assert.NoError(t, f.AddPicture("Sheet1", "A30", filepath.Join("test", "images", "excel.jpg"), &GraphicOptions{AutoFit: true}))
assert.NoError(t, f.AddPicture("Sheet1", "B30", filepath.Join("test", "images", "excel.jpg"), &GraphicOptions{OffsetX: 10, OffsetY: 10, AutoFit: true})) assert.NoError(t, f.AddPicture("Sheet1", "B30", filepath.Join("test", "images", "excel.jpg"), &GraphicOptions{OffsetX: 10, OffsetY: 10, AutoFit: true}))
assert.NoError(t, f.AddPicture("Sheet1", "C30", filepath.Join("test", "images", "excel.jpg"), &GraphicOptions{AutoFit: true, AutoFitIgnoreAspect: true}))
_, err = f.NewSheet("AddPicture") _, err = f.NewSheet("AddPicture")
assert.NoError(t, err) assert.NoError(t, err)
assert.NoError(t, f.SetRowHeight("AddPicture", 10, 30)) assert.NoError(t, f.SetRowHeight("AddPicture", 10, 30))
@ -59,18 +59,55 @@ func TestAddPicture(t *testing.T) {
// Test add picture to worksheet from bytes // Test add picture to worksheet from bytes
assert.NoError(t, f.AddPictureFromBytes("Sheet1", "Q1", &Picture{Extension: ".png", File: file, Format: &GraphicOptions{AltText: "Excel Logo"}})) assert.NoError(t, f.AddPictureFromBytes("Sheet1", "Q1", &Picture{Extension: ".png", File: file, Format: &GraphicOptions{AltText: "Excel Logo"}}))
// Test add picture to worksheet from bytes with unsupported insert type
assert.Equal(t, ErrParameterInvalid, f.AddPictureFromBytes("Sheet1", "Q1", &Picture{Extension: ".png", File: file, Format: &GraphicOptions{AltText: "Excel Logo"}, InsertType: PictureInsertTypePlaceInCell}))
// Test add picture to worksheet from bytes with illegal cell reference // Test add picture to worksheet from bytes with illegal cell reference
assert.EqualError(t, f.AddPictureFromBytes("Sheet1", "A", &Picture{Extension: ".png", File: file, Format: &GraphicOptions{AltText: "Excel Logo"}}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.AddPictureFromBytes("Sheet1", "A", &Picture{Extension: ".png", File: file, Format: &GraphicOptions{AltText: "Excel Logo"}}))
assert.NoError(t, f.AddPicture("Sheet1", "Q8", filepath.Join("test", "images", "excel.gif"), nil)) for _, preset := range [][]string{{"Q8", "gif"}, {"Q15", "jpg"}, {"Q22", "tif"}, {"Q28", "bmp"}} {
assert.NoError(t, f.AddPicture("Sheet1", "Q15", filepath.Join("test", "images", "excel.jpg"), nil)) assert.NoError(t, f.AddPicture("Sheet1", preset[0], filepath.Join("test", "images", fmt.Sprintf("excel.%s", preset[1])), nil))
assert.NoError(t, f.AddPicture("Sheet1", "Q22", filepath.Join("test", "images", "excel.tif"), nil)) }
assert.NoError(t, f.AddPicture("Sheet1", "Q28", filepath.Join("test", "images", "excel.bmp"), nil))
// Test write file to given path // Test write file to given path
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddPicture1.xlsx"))) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddPicture1.xlsx")))
assert.NoError(t, f.Close()) assert.NoError(t, f.Close())
// Test get pictures after inserting a new picture from a workbook which contains existing pictures
f, err = OpenFile(filepath.Join("test", "TestAddPicture1.xlsx"))
assert.NoError(t, err)
assert.NoError(t, f.AddPicture("Sheet1", "A30", filepath.Join("test", "images", "excel.jpg"), nil))
pics, err := f.GetPictures("Sheet1", "A30")
assert.NoError(t, err)
assert.Len(t, pics, 2)
// Test get picture cells
cells, err := f.GetPictureCells("Sheet1")
assert.NoError(t, err)
assert.Equal(t, []string{"F21", "A30", "B30", "C30", "Q1", "Q8", "Q15", "Q22", "Q28"}, cells)
assert.NoError(t, f.Close())
f, err = OpenFile(filepath.Join("test", "TestAddPicture1.xlsx"))
assert.NoError(t, err)
path := "xl/drawings/drawing1.xml"
f.Drawings.Delete(path)
cells, err = f.GetPictureCells("Sheet1")
assert.NoError(t, err)
assert.Equal(t, []string{"F21", "A30", "B30", "C30", "Q1", "Q8", "Q15", "Q22", "Q28"}, cells)
// Test get picture cells with unsupported charset
f.Drawings.Delete(path)
f.Pkg.Store(path, MacintoshCyrillicCharset)
_, err = f.GetPictureCells("Sheet1")
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
assert.NoError(t, f.Close())
f, err = OpenFile(filepath.Join("test", "TestAddPicture1.xlsx"))
assert.NoError(t, err)
// Test get picture cells with unsupported charset
f.Pkg.Store(path, MacintoshCyrillicCharset)
_, err = f.GetPictureCells("Sheet1")
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
assert.NoError(t, f.Close())
// Test add picture with unsupported charset content types // Test add picture with unsupported charset content types
f = NewFile() f = NewFile()
f.ContentTypes = nil f.ContentTypes = nil
@ -99,16 +136,11 @@ func TestAddPictureErrors(t *testing.T) {
// Test add picture with custom image decoder and encoder // Test add picture with custom image decoder and encoder
decode := func(r io.Reader) (image.Image, error) { return nil, nil } decode := func(r io.Reader) (image.Image, error) { return nil, nil }
decodeConfig := func(r io.Reader) (image.Config, error) { return image.Config{Height: 100, Width: 90}, nil } decodeConfig := func(r io.Reader) (image.Config, error) { return image.Config{Height: 100, Width: 90}, nil }
image.RegisterFormat("emf", "", decode, decodeConfig) for cell, ext := range map[string]string{"Q1": "emf", "Q7": "wmf", "Q13": "emz", "Q19": "wmz"} {
image.RegisterFormat("wmf", "", decode, decodeConfig) image.RegisterFormat(ext, "", decode, decodeConfig)
image.RegisterFormat("emz", "", decode, decodeConfig) assert.NoError(t, f.AddPicture("Sheet1", cell, filepath.Join("test", "images", fmt.Sprintf("excel.%s", ext)), nil))
image.RegisterFormat("wmz", "", decode, decodeConfig) }
image.RegisterFormat("svg", "", decode, decodeConfig) assert.NoError(t, f.AddPicture("Sheet1", "Q25", "excelize.svg", &GraphicOptions{ScaleX: 2.8}))
assert.NoError(t, f.AddPicture("Sheet1", "Q1", filepath.Join("test", "images", "excel.emf"), nil))
assert.NoError(t, f.AddPicture("Sheet1", "Q7", filepath.Join("test", "images", "excel.wmf"), nil))
assert.NoError(t, f.AddPicture("Sheet1", "Q13", filepath.Join("test", "images", "excel.emz"), nil))
assert.NoError(t, f.AddPicture("Sheet1", "Q19", filepath.Join("test", "images", "excel.wmz"), nil))
assert.NoError(t, f.AddPicture("Sheet1", "Q25", "excelize.svg", &GraphicOptions{ScaleX: 2.1}))
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddPicture2.xlsx"))) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddPicture2.xlsx")))
assert.NoError(t, f.Close()) assert.NoError(t, f.Close())
} }
@ -120,6 +152,7 @@ func TestGetPicture(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, pics[0].File, 13233) assert.Len(t, pics[0].File, 13233)
assert.Empty(t, pics[0].Format.AltText) assert.Empty(t, pics[0].Format.AltText)
assert.Equal(t, PictureInsertTypePlaceOverCells, pics[0].InsertType)
f, err = prepareTestBook1() f, err = prepareTestBook1()
if !assert.NoError(t, err) { if !assert.NoError(t, err) {
@ -135,7 +168,7 @@ func TestGetPicture(t *testing.T) {
// Try to get picture from a worksheet with illegal cell reference // Try to get picture from a worksheet with illegal cell reference
_, err = f.GetPictures("Sheet1", "A") _, err = f.GetPictures("Sheet1", "A")
assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), err)
// Try to get picture from a worksheet that doesn't contain any images // Try to get picture from a worksheet that doesn't contain any images
pics, err = f.GetPictures("Sheet3", "I9") pics, err = f.GetPictures("Sheet3", "I9")
@ -175,6 +208,32 @@ func TestGetPicture(t *testing.T) {
assert.Len(t, pics, 0) assert.Len(t, pics, 0)
assert.NoError(t, f.Close()) assert.NoError(t, f.Close())
// Try to get picture with one cell anchor
f, err = OpenFile(filepath.Join("test", "TestGetPicture.xlsx"))
assert.NoError(t, err)
f.Pkg.Store("xl/drawings/drawing2.xml", []byte(`<xdr:wsDr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:xdr="http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><xdr:oneCellAnchor><xdr:from><xdr:col>10</xdr:col><xdr:row>15</xdr:row></xdr:from><xdr:to><xdr:col>13</xdr:col><xdr:row>22</xdr:row></xdr:to><xdr:pic><xdr:nvPicPr><xdr:cNvPr id="2"></xdr:cNvPr></xdr:nvPicPr><xdr:blipFill><a:blip r:embed="rId1"></a:blip></xdr:blipFill></xdr:pic></xdr:oneCellAnchor></xdr:wsDr>`))
pics, err = f.GetPictures("Sheet2", "K16")
assert.NoError(t, err)
assert.Len(t, pics, 1)
// Try to get picture cells with one cell anchor
cells, err := f.GetPictureCells("Sheet2")
assert.NoError(t, err)
assert.Equal(t, []string{"K16"}, cells)
// Try to get picture cells with absolute target path in the drawing relationship
rels, err := f.relsReader("xl/drawings/_rels/drawing2.xml.rels")
assert.NoError(t, err)
rels.Relationships[0].Target = "/xl/media/image2.jpeg"
cells, err = f.GetPictureCells("Sheet2")
assert.NoError(t, err)
assert.Equal(t, []string{"K16"}, cells)
// Try to get pictures with absolute target path in the drawing relationship
pics, err = f.GetPictures("Sheet2", "K16")
assert.NoError(t, err)
assert.Len(t, pics, 1)
assert.NoError(t, f.Close())
// Test get picture from none drawing worksheet // Test get picture from none drawing worksheet
f = NewFile() f = NewFile()
pics, err = f.GetPictures("Sheet1", "F22") pics, err = f.GetPictures("Sheet1", "F22")
@ -185,12 +244,44 @@ func TestGetPicture(t *testing.T) {
// Test get pictures with unsupported charset // Test get pictures with unsupported charset
path := "xl/drawings/drawing1.xml" path := "xl/drawings/drawing1.xml"
f.Drawings.Delete(path)
f.Pkg.Store(path, MacintoshCyrillicCharset) f.Pkg.Store(path, MacintoshCyrillicCharset)
_, err = f.GetPictures("Sheet1", "F21")
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
_, err = f.getPicture(20, 5, path, "xl/drawings/_rels/drawing2.xml.rels") _, err = f.getPicture(20, 5, path, "xl/drawings/_rels/drawing2.xml.rels")
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
f.Drawings.Delete(path) f.Drawings.Delete(path)
_, err = f.getPicture(20, 5, path, "xl/drawings/_rels/drawing2.xml.rels") _, err = f.getPicture(20, 5, path, "xl/drawings/_rels/drawing2.xml.rels")
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
assert.NoError(t, f.Close())
// Test get embedded cell pictures
f, err = OpenFile(filepath.Join("test", "TestGetPicture.xlsx"))
assert.NoError(t, err)
assert.NoError(t, f.SetCellFormula("Sheet1", "F21", "=_xlfn.DISPIMG(\"ID_********************************\",1)"))
f.Pkg.Store(defaultXMLPathCellImages, []byte(`<etc:cellImages xmlns:etc="http://www.wps.cn/officeDocument/2017/etCustomData"><etc:cellImage><xdr:pic><xdr:nvPicPr><xdr:cNvPr id="1" name="ID_********************************" descr="CellImage1"/></xdr:nvPicPr><xdr:blipFill><a:blip r:embed="rId1"/></xdr:blipFill></xdr:pic></etc:cellImage></etc:cellImages>`))
f.Pkg.Store(defaultXMLPathCellImagesRels, []byte(fmt.Sprintf(`<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="%s" Target="media/image1.jpeg"/></Relationships>`, SourceRelationshipImage)))
pics, err = f.GetPictures("Sheet1", "F21")
assert.NoError(t, err)
assert.Len(t, pics, 2)
assert.Equal(t, "CellImage1", pics[0].Format.AltText)
assert.Equal(t, PictureInsertTypeDISPIMG, pics[0].InsertType)
// Test get embedded cell pictures with invalid formula
assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "=_xlfn.DISPIMG()"))
_, err = f.GetPictures("Sheet1", "A1")
assert.EqualError(t, err, "DISPIMG requires 2 numeric arguments")
// Test get embedded cell pictures with unsupported charset
f.Relationships.Delete(defaultXMLPathCellImagesRels)
f.Pkg.Store(defaultXMLPathCellImagesRels, MacintoshCyrillicCharset)
_, err = f.GetPictures("Sheet1", "F21")
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
f.Pkg.Store(defaultXMLPathCellImages, MacintoshCyrillicCharset)
f.DecodeCellImages = nil
_, err = f.GetPictures("Sheet1", "F21")
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
assert.NoError(t, f.Close())
} }
func TestAddDrawingPicture(t *testing.T) { func TestAddDrawingPicture(t *testing.T) {
@ -198,6 +289,8 @@ func TestAddDrawingPicture(t *testing.T) {
f := NewFile() f := NewFile()
opts := &GraphicOptions{PrintObject: boolPtr(true), Locked: boolPtr(false)} opts := &GraphicOptions{PrintObject: boolPtr(true), Locked: boolPtr(false)}
assert.EqualError(t, f.addDrawingPicture("sheet1", "", "A", "", 0, 0, image.Config{}, opts), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.EqualError(t, f.addDrawingPicture("sheet1", "", "A", "", 0, 0, image.Config{}, opts), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
// Test addDrawingPicture with invalid positioning types
assert.Equal(t, f.addDrawingPicture("sheet1", "", "A1", "", 0, 0, image.Config{}, &GraphicOptions{Positioning: "x"}), ErrParameterInvalid)
path := "xl/drawings/drawing1.xml" path := "xl/drawings/drawing1.xml"
f.Pkg.Store(path, MacintoshCyrillicCharset) f.Pkg.Store(path, MacintoshCyrillicCharset)
@ -227,19 +320,60 @@ func TestAddPictureFromBytes(t *testing.T) {
func TestDeletePicture(t *testing.T) { func TestDeletePicture(t *testing.T) {
f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
assert.NoError(t, err) assert.NoError(t, err)
// Test delete picture on a worksheet which does not contains any pictures
assert.NoError(t, f.DeletePicture("Sheet1", "A1")) assert.NoError(t, f.DeletePicture("Sheet1", "A1"))
assert.NoError(t, f.AddPicture("Sheet1", "P1", filepath.Join("test", "images", "excel.jpg"), nil)) // Add same pictures on different worksheets
assert.NoError(t, f.DeletePicture("Sheet1", "P1")) assert.NoError(t, f.AddPicture("Sheet1", "F20", filepath.Join("test", "images", "excel.jpg"), nil))
assert.NoError(t, f.AddPicture("Sheet1", "I20", filepath.Join("test", "images", "excel.jpg"), nil))
assert.NoError(t, f.AddPicture("Sheet2", "F1", filepath.Join("test", "images", "excel.jpg"), nil))
// Test delete picture on a worksheet, the images should be preserved
assert.NoError(t, f.DeletePicture("Sheet1", "F20"))
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDeletePicture.xlsx"))) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDeletePicture.xlsx")))
assert.NoError(t, f.Close())
f, err = OpenFile(filepath.Join("test", "TestDeletePicture.xlsx"))
assert.NoError(t, err)
// Test delete same picture on different worksheet, the images should be removed
assert.NoError(t, f.DeletePicture("Sheet1", "F10"))
assert.NoError(t, f.DeletePicture("Sheet2", "F1"))
assert.NoError(t, f.DeletePicture("Sheet1", "I20"))
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDeletePicture2.xlsx")))
// Test delete picture on not exists worksheet // Test delete picture on not exists worksheet
assert.EqualError(t, f.DeletePicture("SheetN", "A1"), "sheet SheetN does not exist") assert.EqualError(t, f.DeletePicture("SheetN", "A1"), "sheet SheetN does not exist")
// Test delete picture with invalid sheet name // Test delete picture with invalid sheet name
assert.EqualError(t, f.DeletePicture("Sheet:1", "A1"), ErrSheetNameInvalid.Error()) assert.Equal(t, ErrSheetNameInvalid, f.DeletePicture("Sheet:1", "A1"))
// Test delete picture with invalid coordinates // Test delete picture with invalid coordinates
assert.EqualError(t, f.DeletePicture("Sheet1", ""), newCellNameToCoordinatesError("", newInvalidCellNameError("")).Error()) assert.Equal(t, newCellNameToCoordinatesError("", newInvalidCellNameError("")), f.DeletePicture("Sheet1", ""))
assert.NoError(t, f.Close()) assert.NoError(t, f.Close())
// Test delete picture on no chart worksheet // Test delete picture on no chart worksheet
assert.NoError(t, NewFile().DeletePicture("Sheet1", "A1")) assert.NoError(t, NewFile().DeletePicture("Sheet1", "A1"))
f, err = OpenFile(filepath.Join("test", "TestDeletePicture.xlsx"))
assert.NoError(t, err)
// Test delete picture with unsupported charset drawing
f.Pkg.Store("xl/drawings/drawing1.xml", MacintoshCyrillicCharset)
assert.EqualError(t, f.DeletePicture("Sheet1", "F10"), "XML syntax error on line 1: invalid UTF-8")
assert.NoError(t, f.Close())
f, err = OpenFile(filepath.Join("test", "TestDeletePicture.xlsx"))
assert.NoError(t, err)
// Test delete picture with unsupported charset drawing relationships
f.Relationships.Delete("xl/drawings/_rels/drawing1.xml.rels")
f.Pkg.Store("xl/drawings/_rels/drawing1.xml.rels", MacintoshCyrillicCharset)
assert.NoError(t, f.DeletePicture("Sheet2", "F1"))
assert.NoError(t, f.Close())
f = NewFile()
assert.NoError(t, err)
assert.NoError(t, f.AddPicture("Sheet1", "A1", filepath.Join("test", "images", "excel.jpg"), nil))
assert.NoError(t, f.AddPicture("Sheet1", "G1", filepath.Join("test", "images", "excel.jpg"), nil))
drawing, ok := f.Drawings.Load("xl/drawings/drawing1.xml")
assert.True(t, ok)
// Made two picture reference the same drawing relationship ID
drawing.(*xlsxWsDr).TwoCellAnchor[1].Pic.BlipFill.Blip.Embed = "rId1"
assert.NoError(t, f.DeletePicture("Sheet1", "A1"))
assert.NoError(t, f.Close())
} }
func TestDrawingResize(t *testing.T) { func TestDrawingResize(t *testing.T) {
@ -249,11 +383,22 @@ func TestDrawingResize(t *testing.T) {
assert.EqualError(t, err, "sheet SheetN does not exist") assert.EqualError(t, err, "sheet SheetN does not exist")
// Test calculate drawing resize with invalid coordinates // Test calculate drawing resize with invalid coordinates
_, _, _, _, err = f.drawingResize("Sheet1", "", 1, 1, nil) _, _, _, _, err = f.drawingResize("Sheet1", "", 1, 1, nil)
assert.EqualError(t, err, newCellNameToCoordinatesError("", newInvalidCellNameError("")).Error()) assert.Equal(t, newCellNameToCoordinatesError("", newInvalidCellNameError("")), err)
ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml") ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
assert.True(t, ok) assert.True(t, ok)
ws.(*xlsxWorksheet).MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:A"}}} ws.(*xlsxWorksheet).MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: "A:A"}}}
assert.EqualError(t, f.AddPicture("Sheet1", "A1", filepath.Join("test", "images", "excel.jpg"), &GraphicOptions{AutoFit: true}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.AddPicture("Sheet1", "A1", filepath.Join("test", "images", "excel.jpg"), &GraphicOptions{AutoFit: true}))
}
func TestSetContentTypePartRelsExtensions(t *testing.T) {
f := NewFile()
f.ContentTypes = &xlsxTypes{}
assert.NoError(t, f.setContentTypePartRelsExtensions())
// Test set content type part relationships extensions with unsupported charset content types
f.ContentTypes = nil
f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
assert.EqualError(t, f.setContentTypePartRelsExtensions(), "XML syntax error on line 1: invalid UTF-8")
} }
func TestSetContentTypePartImageExtensions(t *testing.T) { func TestSetContentTypePartImageExtensions(t *testing.T) {
@ -279,3 +424,175 @@ func TestAddContentTypePart(t *testing.T) {
f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
assert.EqualError(t, f.addContentTypePart(0, "unknown"), "XML syntax error on line 1: invalid UTF-8") assert.EqualError(t, f.addContentTypePart(0, "unknown"), "XML syntax error on line 1: invalid UTF-8")
} }
func TestGetPictureCells(t *testing.T) {
f := NewFile()
// Test get picture cells on a worksheet which not contains any pictures
cells, err := f.GetPictureCells("Sheet1")
assert.NoError(t, err)
assert.Empty(t, cells)
// Test get picture cells on not exists worksheet
_, err = f.GetPictureCells("SheetN")
assert.EqualError(t, err, "sheet SheetN does not exist")
assert.NoError(t, f.Close())
// Test get embedded picture cells
f = NewFile()
assert.NoError(t, f.AddPicture("Sheet1", "A1", filepath.Join("test", "images", "excel.png"), nil))
assert.NoError(t, f.SetCellFormula("Sheet1", "A2", "=_xlfn.DISPIMG(\"ID_********************************\",1)"))
cells, err = f.GetPictureCells("Sheet1")
assert.NoError(t, err)
assert.Equal(t, []string{"A2", "A1"}, cells)
// Test get embedded cell pictures with invalid formula
assert.NoError(t, f.SetCellFormula("Sheet1", "A2", "=_xlfn.DISPIMG()"))
_, err = f.GetPictureCells("Sheet1")
assert.EqualError(t, err, "DISPIMG requires 2 numeric arguments")
assert.NoError(t, f.Close())
}
func TestExtractDecodeCellAnchor(t *testing.T) {
f := NewFile()
cond := func(a *decodeFrom) bool { return true }
cb := func(a *decodeCellAnchor, r *xlsxRelationship) {}
f.extractDecodeCellAnchor(&xdrCellAnchor{GraphicFrame: string(MacintoshCyrillicCharset)}, "", cond, cb)
}
func TestGetCellImages(t *testing.T) {
f := NewFile()
f.Sheet.Delete("xl/worksheets/sheet1.xml")
f.Pkg.Store("xl/worksheets/sheet1.xml", MacintoshCyrillicCharset)
_, err := f.getCellImages("Sheet1", "A1")
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
assert.NoError(t, f.Close())
// Test get the cell images
prepareWorkbook := func() *File {
f := NewFile()
assert.NoError(t, f.AddPicture("Sheet1", "A1", filepath.Join("test", "images", "excel.png"), nil))
f.Pkg.Store(defaultXMLMetadata, []byte(`<metadata><valueMetadata count="1"><bk><rc t="1" v="0"/></bk></valueMetadata></metadata>`))
f.Pkg.Store(defaultXMLRdRichValuePart, []byte(`<rvData count="1"><rv s="0"><v>0</v><v>5</v></rv></rvData>`))
f.Pkg.Store(defaultXMLRdRichValueRel, []byte(`<richValueRels><rel r:id="rId1"/></richValueRels>`))
f.Pkg.Store(defaultXMLRdRichValueRelRels, []byte(fmt.Sprintf(`<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="%s" Target="../media/image1.png"/></Relationships>`, SourceRelationshipImage)))
f.Sheet.Store("xl/worksheets/sheet1.xml", &xlsxWorksheet{
SheetData: xlsxSheetData{Row: []xlsxRow{
{R: 1, C: []xlsxC{{R: "A1", T: "e", V: formulaErrorVALUE, Vm: uintPtr(1)}}},
}},
})
return f
}
f = prepareWorkbook()
pics, err := f.GetPictures("Sheet1", "A1")
assert.NoError(t, err)
assert.Equal(t, 1, len(pics))
assert.Equal(t, PictureInsertTypePlaceInCell, pics[0].InsertType)
cells, err := f.GetPictureCells("Sheet1")
assert.NoError(t, err)
assert.Equal(t, []string{"A1"}, cells)
// Test get the cell images without image relationships parts
f.Relationships.Delete(defaultXMLRdRichValueRelRels)
f.Pkg.Store(defaultXMLRdRichValueRelRels, []byte(fmt.Sprintf(`<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="%s" Target="../media/image1.png"/></Relationships>`, SourceRelationshipHyperLink)))
pics, err = f.GetPictures("Sheet1", "A1")
assert.NoError(t, err)
assert.Empty(t, pics)
// Test get the cell images with unsupported charset rich data rich value relationships
f.Relationships.Delete(defaultXMLRdRichValueRelRels)
f.Pkg.Store(defaultXMLRdRichValueRelRels, MacintoshCyrillicCharset)
pics, err = f.GetPictures("Sheet1", "A1")
assert.NoError(t, err)
assert.Empty(t, pics)
// Test get the cell images with unsupported charset rich data rich value
f.Pkg.Store(defaultXMLRdRichValueRel, MacintoshCyrillicCharset)
_, err = f.GetPictures("Sheet1", "A1")
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
// Test get the image cells without block of metadata records
cells, err = f.GetPictureCells("Sheet1")
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
assert.Empty(t, cells)
// Test get the cell images with rich data rich value relationships
f.Pkg.Store(defaultXMLMetadata, []byte(`<metadata><valueMetadata count="1"><bk><rc t="1" v="0"/></bk></valueMetadata></metadata>`))
f.Pkg.Store(defaultXMLRdRichValueRel, []byte(`<richValueRels/>`))
pics, err = f.GetPictures("Sheet1", "A1")
assert.NoError(t, err)
assert.Empty(t, pics)
// Test get the cell images with unsupported charset meta data
f.Pkg.Store(defaultXMLMetadata, MacintoshCyrillicCharset)
_, err = f.GetPictures("Sheet1", "A1")
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
// Test get the cell images without block of metadata records
f.Pkg.Store(defaultXMLMetadata, []byte(`<metadata><valueMetadata/></metadata>`))
pics, err = f.GetPictures("Sheet1", "A1")
assert.NoError(t, err)
assert.Empty(t, pics)
f = prepareWorkbook()
// Test get the cell images with empty image cell rich value
f.Pkg.Store(defaultXMLRdRichValuePart, []byte(`<rvData count="1"><rv s="0"><v></v><v>5</v></rv></rvData>`))
pics, err = f.GetPictures("Sheet1", "A1")
assert.EqualError(t, err, "strconv.Atoi: parsing \"\": invalid syntax")
assert.Empty(t, pics)
// Test get the cell images without image cell rich value
f.Pkg.Store(defaultXMLRdRichValuePart, []byte(`<rvData count="1"><rv s="0"><v>0</v><v>1</v></rv></rvData>`))
pics, err = f.GetPictures("Sheet1", "A1")
assert.NoError(t, err)
assert.Empty(t, pics)
// Test get the cell images with unsupported charset rich value
f.Pkg.Store(defaultXMLRdRichValuePart, MacintoshCyrillicCharset)
_, err = f.GetPictures("Sheet1", "A1")
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
f = prepareWorkbook()
// Test get the cell images with invalid rich value index
f.Pkg.Store(defaultXMLMetadata, []byte(`<metadata><valueMetadata count="1"><bk><rc t="1" v="1"/></bk></valueMetadata></metadata>`))
pics, err = f.GetPictures("Sheet1", "A1")
assert.NoError(t, err)
assert.Empty(t, pics)
f = prepareWorkbook()
// Test get the cell images inserted by IMAGE formula function
f.Pkg.Store(defaultXMLRdRichValuePart, []byte(`<rvData count="1"><rv s="1"><v>0</v><v>1</v><v>0</v><v>0</v></rv></rvData>`))
f.Pkg.Store(defaultXMLRdRichValueWebImagePart, []byte(`<webImagesSrd xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><webImageSrd><address r:id="rId1"/><blip r:id="rId2"/></webImageSrd>
</webImagesSrd>`))
f.Pkg.Store(defaultXMLRdRichValueWebImagePartRels, []byte(fmt.Sprintf(`<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="%s" Target="https://github.com/xuri/excelize" TargetMode="External"/><Relationship Id="rId2" Type="%s" Target="../media/image1.png"/></Relationships>`, SourceRelationshipHyperLink, SourceRelationshipImage)))
pics, err = f.GetPictures("Sheet1", "A1")
assert.NoError(t, err)
assert.Equal(t, 1, len(pics))
assert.Equal(t, PictureInsertTypeIMAGE, pics[0].InsertType)
// Test get the cell images inserted by IMAGE formula function with unsupported charset web images relationships
f.Relationships.Delete(defaultXMLRdRichValueWebImagePartRels)
f.Pkg.Store(defaultXMLRdRichValueWebImagePartRels, MacintoshCyrillicCharset)
pics, err = f.GetPictures("Sheet1", "A1")
assert.NoError(t, err)
assert.Empty(t, pics)
// Test get the cell images inserted by IMAGE formula function without image part
f.Relationships.Delete(defaultXMLRdRichValueWebImagePartRels)
f.Pkg.Store(defaultXMLRdRichValueWebImagePartRels, []byte(fmt.Sprintf(`<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="%s" Target="https://github.com/xuri/excelize" TargetMode="External"/><Relationship Id="rId2" Type="%s" Target="../media/image1.png"/></Relationships>`, SourceRelationshipHyperLink, SourceRelationshipHyperLink)))
pics, err = f.GetPictures("Sheet1", "A1")
assert.NoError(t, err)
assert.Empty(t, pics)
// Test get the cell images inserted by IMAGE formula function with unsupported charset web images part
f.Pkg.Store(defaultXMLRdRichValueWebImagePart, MacintoshCyrillicCharset)
_, err = f.GetPictures("Sheet1", "A1")
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
// Test get the cell images inserted by IMAGE formula function with empty charset web images part
f.Pkg.Store(defaultXMLRdRichValueWebImagePart, []byte(`<webImagesSrd xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" />`))
pics, err = f.GetPictures("Sheet1", "A1")
assert.NoError(t, err)
assert.Empty(t, pics)
// Test get the cell images inserted by IMAGE formula function with invalid rich value index
f.Pkg.Store(defaultXMLRdRichValuePart, []byte(`<rvData count="1"><rv s="1"><v></v><v>1</v><v>0</v><v>0</v></rv></rvData>`))
_, err = f.GetPictures("Sheet1", "A1")
assert.EqualError(t, err, "strconv.Atoi: parsing \"\": invalid syntax")
}
func TestGetImageCells(t *testing.T) {
f := NewFile()
f.Sheet.Delete("xl/worksheets/sheet1.xml")
f.Pkg.Store("xl/worksheets/sheet1.xml", MacintoshCyrillicCharset)
_, err := f.getImageCells("Sheet1")
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
assert.NoError(t, f.Close())
}

View File

@ -1,4 +1,4 @@
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in // this source code is governed by a BSD-style license that can be found in
// the LICENSE file. // the LICENSE file.
// //
@ -7,15 +7,22 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming // Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of // API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.16 or later. // data. This library needs Go version 1.18 or later.
package excelize package excelize
import ( import (
"bytes"
"encoding/xml" "encoding/xml"
"fmt" "fmt"
"io"
"path/filepath"
"reflect"
"strconv" "strconv"
"strings" "strings"
"golang.org/x/text/cases"
"golang.org/x/text/language"
) )
// PivotTableOptions directly maps the format settings of the pivot table. // PivotTableOptions directly maps the format settings of the pivot table.
@ -26,9 +33,14 @@ import (
// PivotStyleMedium1 - PivotStyleMedium28 // PivotStyleMedium1 - PivotStyleMedium28
// PivotStyleDark1 - PivotStyleDark28 // PivotStyleDark1 - PivotStyleDark28
type PivotTableOptions struct { type PivotTableOptions struct {
pivotTableSheetName string pivotTableXML string
pivotCacheXML string
pivotSheetName string
pivotDataRange string
namedDataRange bool
DataRange string DataRange string
PivotTableRange string PivotTableRange string
Name string
Rows []PivotTableField Rows []PivotTableField
Columns []PivotTableField Columns []PivotTableField
Data []PivotTableField Data []PivotTableField
@ -39,6 +51,7 @@ type PivotTableOptions struct {
UseAutoFormatting bool UseAutoFormatting bool
PageOverThenDown bool PageOverThenDown bool
MergeItem bool MergeItem bool
ClassicLayout bool
CompactData bool CompactData bool
ShowError bool ShowError bool
ShowRowHeaders bool ShowRowHeaders bool
@ -46,10 +59,16 @@ type PivotTableOptions struct {
ShowRowStripes bool ShowRowStripes bool
ShowColStripes bool ShowColStripes bool
ShowLastColumn bool ShowLastColumn bool
FieldPrintTitles bool
ItemPrintTitles bool
PivotTableStyleName string PivotTableStyleName string
} }
// PivotTableField directly maps the field settings of the pivot table. // PivotTableField directly maps the field settings of the pivot table.
//
// Name specifies the name of the data field. Maximum 255 characters
// are allowed in data field name, excess characters will be truncated.
//
// Subtotal specifies the aggregation function that applies to this data // Subtotal specifies the aggregation function that applies to this data
// field. The default value is sum. The possible values for this attribute // field. The default value is sum. The possible values for this attribute
// are: // are:
@ -66,24 +85,28 @@ type PivotTableOptions struct {
// Var // Var
// Varp // Varp
// //
// Name specifies the name of the data field. Maximum 255 characters // NumFmt specifies the number format ID of the data field, this filed only
// are allowed in data field name, excess characters will be truncated. // accepts built-in number format ID and does not support custom number format
// expression currently.
type PivotTableField struct { type PivotTableField struct {
Compact bool Compact bool
Data string Data string
Name string Name string
Outline bool Outline bool
ShowAll bool
InsertBlankRow bool
Subtotal string Subtotal string
DefaultSubtotal bool DefaultSubtotal bool
NumFmt int
} }
// AddPivotTable provides the method to add pivot table by given pivot table // AddPivotTable provides the method to add pivot table by given pivot table
// options. Note that the same fields can not in Columns, Rows and Filter // options. Note that the same fields can not in Columns, Rows and Filter
// fields at the same time. // fields at the same time.
// //
// For example, create a pivot table on the range reference Sheet1!$G$2:$M$34 // For example, create a pivot table on the range reference Sheet1!G2:M34 with
// with the range reference Sheet1!$A$1:$E$31 as the data source, summarize by // the range reference Sheet1!A1:E31 as the data source, summarize by sum for
// sum for sales: // sales:
// //
// package main // package main
// //
@ -115,8 +138,8 @@ type PivotTableField struct {
// f.SetCellValue("Sheet1", fmt.Sprintf("E%d", row), region[rand.Intn(4)]) // f.SetCellValue("Sheet1", fmt.Sprintf("E%d", row), region[rand.Intn(4)])
// } // }
// if err := f.AddPivotTable(&excelize.PivotTableOptions{ // if err := f.AddPivotTable(&excelize.PivotTableOptions{
// DataRange: "Sheet1!$A$1:$E$31", // DataRange: "Sheet1!A1:E31",
// PivotTableRange: "Sheet1!$G$2:$M$34", // PivotTableRange: "Sheet1!G2:M34",
// Rows: []excelize.PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, // Rows: []excelize.PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
// Filter: []excelize.PivotTableField{{Data: "Region"}}, // Filter: []excelize.PivotTableField{{Data: "Region"}},
// Columns: []excelize.PivotTableField{{Data: "Type", DefaultSubtotal: true}}, // Columns: []excelize.PivotTableField{{Data: "Type", DefaultSubtotal: true}},
@ -145,22 +168,20 @@ func (f *File) AddPivotTable(opts *PivotTableOptions) error {
pivotCacheID := f.countPivotCache() + 1 pivotCacheID := f.countPivotCache() + 1
sheetRelationshipsPivotTableXML := "../pivotTables/pivotTable" + strconv.Itoa(pivotTableID) + ".xml" sheetRelationshipsPivotTableXML := "../pivotTables/pivotTable" + strconv.Itoa(pivotTableID) + ".xml"
pivotTableXML := strings.ReplaceAll(sheetRelationshipsPivotTableXML, "..", "xl") opts.pivotTableXML = strings.ReplaceAll(sheetRelationshipsPivotTableXML, "..", "xl")
pivotCacheXML := "xl/pivotCache/pivotCacheDefinition" + strconv.Itoa(pivotCacheID) + ".xml" opts.pivotCacheXML = "xl/pivotCache/pivotCacheDefinition" + strconv.Itoa(pivotCacheID) + ".xml"
err = f.addPivotCache(pivotCacheXML, opts) if err = f.addPivotCache(opts); err != nil {
if err != nil {
return err return err
} }
// workbook pivot cache // workbook pivot cache
workBookPivotCacheRID := f.addRels(f.getWorkbookRelsPath(), SourceRelationshipPivotCache, fmt.Sprintf("/xl/pivotCache/pivotCacheDefinition%d.xml", pivotCacheID), "") workBookPivotCacheRID := f.addRels(f.getWorkbookRelsPath(), SourceRelationshipPivotCache, strings.TrimPrefix(opts.pivotCacheXML, "xl/"), "")
cacheID := f.addWorkbookPivotCache(workBookPivotCacheRID) cacheID := f.addWorkbookPivotCache(workBookPivotCacheRID)
pivotCacheRels := "xl/pivotTables/_rels/pivotTable" + strconv.Itoa(pivotTableID) + ".xml.rels" pivotCacheRels := "xl/pivotTables/_rels/pivotTable" + strconv.Itoa(pivotTableID) + ".xml.rels"
// rId not used // rId not used
_ = f.addRels(pivotCacheRels, SourceRelationshipPivotCache, fmt.Sprintf("../pivotCache/pivotCacheDefinition%d.xml", pivotCacheID), "") _ = f.addRels(pivotCacheRels, SourceRelationshipPivotCache, fmt.Sprintf("../pivotCache/pivotCacheDefinition%d.xml", pivotCacheID), "")
err = f.addPivotTable(cacheID, pivotTableID, pivotTableXML, opts) if err = f.addPivotTable(cacheID, pivotTableID, opts); err != nil {
if err != nil {
return err return err
} }
pivotTableSheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(pivotTableSheetPath, "xl/worksheets/") + ".rels" pivotTableSheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(pivotTableSheetPath, "xl/worksheets/") + ".rels"
@ -179,16 +200,18 @@ func (f *File) parseFormatPivotTableSet(opts *PivotTableOptions) (*xlsxWorksheet
} }
pivotTableSheetName, _, err := f.adjustRange(opts.PivotTableRange) pivotTableSheetName, _, err := f.adjustRange(opts.PivotTableRange)
if err != nil { if err != nil {
return nil, "", fmt.Errorf("parameter 'PivotTableRange' parsing error: %s", err.Error()) return nil, "", newPivotTableRangeError(err.Error())
} }
opts.pivotTableSheetName = pivotTableSheetName if len(opts.Name) > MaxFieldLength {
dataRange := f.getDefinedNameRefTo(opts.DataRange, pivotTableSheetName) return nil, "", ErrNameLength
if dataRange == "" {
dataRange = opts.DataRange
} }
dataSheetName, _, err := f.adjustRange(dataRange) opts.pivotSheetName = pivotTableSheetName
if err = f.getPivotTableDataRange(opts); err != nil {
return nil, "", err
}
dataSheetName, _, err := f.adjustRange(opts.pivotDataRange)
if err != nil { if err != nil {
return nil, "", fmt.Errorf("parameter 'DataRange' parsing error: %s", err.Error()) return nil, "", newPivotTableDataRangeError(err.Error())
} }
dataSheet, err := f.workSheetReader(dataSheetName) dataSheet, err := f.workSheetReader(dataSheetName)
if err != nil { if err != nil {
@ -196,7 +219,10 @@ func (f *File) parseFormatPivotTableSet(opts *PivotTableOptions) (*xlsxWorksheet
} }
pivotTableSheetPath, ok := f.getSheetXMLPath(pivotTableSheetName) pivotTableSheetPath, ok := f.getSheetXMLPath(pivotTableSheetName)
if !ok { if !ok {
return dataSheet, pivotTableSheetPath, fmt.Errorf("sheet %s does not exist", pivotTableSheetName) return dataSheet, pivotTableSheetPath, ErrSheetNotExist{pivotTableSheetName}
}
if opts.CompactData && opts.ClassicLayout {
return nil, "", ErrPivotTableClassicLayout
} }
return dataSheet, pivotTableSheetPath, err return dataSheet, pivotTableSheetPath, err
} }
@ -231,17 +257,16 @@ func (f *File) adjustRange(rangeStr string) (string, []int, error) {
return rng[0], []int{x1, y1, x2, y2}, nil return rng[0], []int{x1, y1, x2, y2}, nil
} }
// getPivotFieldsOrder provides a function to get order list of pivot table // getTableFieldsOrder provides a function to get order list of pivot table
// fields. // fields.
func (f *File) getPivotFieldsOrder(opts *PivotTableOptions) ([]string, error) { func (f *File) getTableFieldsOrder(opts *PivotTableOptions) ([]string, error) {
var order []string var order []string
dataRange := f.getDefinedNameRefTo(opts.DataRange, opts.pivotTableSheetName) if err := f.getPivotTableDataRange(opts); err != nil {
if dataRange == "" { return order, err
dataRange = opts.DataRange
} }
dataSheet, coordinates, err := f.adjustRange(dataRange) dataSheet, coordinates, err := f.adjustRange(opts.pivotDataRange)
if err != nil { if err != nil {
return order, fmt.Errorf("parameter 'DataRange' parsing error: %s", err.Error()) return order, newPivotTableDataRangeError(err.Error())
} }
for col := coordinates[0]; col <= coordinates[2]; col++ { for col := coordinates[0]; col <= coordinates[2]; col++ {
coordinate, _ := CoordinatesToCellName(col, coordinates[1]) coordinate, _ := CoordinatesToCellName(col, coordinates[1])
@ -249,83 +274,68 @@ func (f *File) getPivotFieldsOrder(opts *PivotTableOptions) ([]string, error) {
if err != nil { if err != nil {
return order, err return order, err
} }
if name == "" {
return order, ErrParameterInvalid
}
order = append(order, name) order = append(order, name)
} }
return order, nil return order, nil
} }
// addPivotCache provides a function to create a pivot cache by given properties. // addPivotCache provides a function to create a pivot cache by given properties.
func (f *File) addPivotCache(pivotCacheXML string, opts *PivotTableOptions) error { func (f *File) addPivotCache(opts *PivotTableOptions) error {
// validate data range // validate data range
definedNameRef := true dataSheet, coordinates, err := f.adjustRange(opts.pivotDataRange)
dataRange := f.getDefinedNameRefTo(opts.DataRange, opts.pivotTableSheetName)
if dataRange == "" {
definedNameRef = false
dataRange = opts.DataRange
}
dataSheet, coordinates, err := f.adjustRange(dataRange)
if err != nil { if err != nil {
return fmt.Errorf("parameter 'DataRange' parsing error: %s", err.Error()) return newPivotTableDataRangeError(err.Error())
} }
// data range has been checked order, err := f.getTableFieldsOrder(opts)
order, _ := f.getPivotFieldsOrder(opts) if err != nil {
hCell, _ := CoordinatesToCellName(coordinates[0], coordinates[1]) return newPivotTableDataRangeError(err.Error())
vCell, _ := CoordinatesToCellName(coordinates[2], coordinates[3]) }
topLeftCell, _ := CoordinatesToCellName(coordinates[0], coordinates[1])
bottomRightCell, _ := CoordinatesToCellName(coordinates[2], coordinates[3])
pc := xlsxPivotCacheDefinition{ pc := xlsxPivotCacheDefinition{
SaveData: false, SaveData: false,
RefreshOnLoad: true, RefreshOnLoad: true,
CreatedVersion: pivotTableVersion, CreatedVersion: pivotTableVersion,
RefreshedVersion: pivotTableVersion, RefreshedVersion: pivotTableRefreshedVersion,
MinRefreshableVersion: pivotTableVersion, MinRefreshableVersion: pivotTableVersion,
CacheSource: &xlsxCacheSource{ CacheSource: &xlsxCacheSource{
Type: "worksheet", Type: "worksheet",
WorksheetSource: &xlsxWorksheetSource{ WorksheetSource: &xlsxWorksheetSource{
Ref: hCell + ":" + vCell, Ref: topLeftCell + ":" + bottomRightCell,
Sheet: dataSheet, Sheet: dataSheet,
}, },
}, },
CacheFields: &xlsxCacheFields{}, CacheFields: &xlsxCacheFields{},
} }
if definedNameRef { if opts.namedDataRange {
pc.CacheSource.WorksheetSource = &xlsxWorksheetSource{Name: opts.DataRange} pc.CacheSource.WorksheetSource = &xlsxWorksheetSource{Name: opts.DataRange}
} }
for _, name := range order { for _, name := range order {
rowOptions, rowOk := f.getPivotTableFieldOptions(name, opts.Rows)
columnOptions, colOk := f.getPivotTableFieldOptions(name, opts.Columns)
sharedItems := xlsxSharedItems{
Count: 0,
}
s := xlsxString{}
if (rowOk && !rowOptions.DefaultSubtotal) || (colOk && !columnOptions.DefaultSubtotal) {
s = xlsxString{
V: "",
}
sharedItems.Count++
sharedItems.S = &s
}
pc.CacheFields.CacheField = append(pc.CacheFields.CacheField, &xlsxCacheField{ pc.CacheFields.CacheField = append(pc.CacheFields.CacheField, &xlsxCacheField{
Name: name, Name: name,
SharedItems: &sharedItems, SharedItems: &xlsxSharedItems{ContainsBlank: true, M: []xlsxMissing{{}}},
}) })
} }
pc.CacheFields.Count = len(pc.CacheFields.CacheField) pc.CacheFields.Count = len(pc.CacheFields.CacheField)
pivotCache, err := xml.Marshal(pc) pivotCache, err := xml.Marshal(pc)
f.saveFileList(pivotCacheXML, pivotCache) f.saveFileList(opts.pivotCacheXML, pivotCache)
return err return err
} }
// addPivotTable provides a function to create a pivot table by given pivot // addPivotTable provides a function to create a pivot table by given pivot
// table ID and properties. // table ID and properties.
func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, opts *PivotTableOptions) error { func (f *File) addPivotTable(cacheID, pivotTableID int, opts *PivotTableOptions) error {
// validate pivot table range // validate pivot table range
_, coordinates, err := f.adjustRange(opts.PivotTableRange) _, coordinates, err := f.adjustRange(opts.PivotTableRange)
if err != nil { if err != nil {
return fmt.Errorf("parameter 'PivotTableRange' parsing error: %s", err.Error()) return newPivotTableRangeError(err.Error())
} }
hCell, _ := CoordinatesToCellName(coordinates[0], coordinates[1]) topLeftCell, _ := CoordinatesToCellName(coordinates[0], coordinates[1])
vCell, _ := CoordinatesToCellName(coordinates[2], coordinates[3]) bottomRightCell, _ := CoordinatesToCellName(coordinates[2], coordinates[3])
pivotTableStyle := func() string { pivotTableStyle := func() string {
if opts.PivotTableStyleName == "" { if opts.PivotTableStyleName == "" {
@ -334,11 +344,11 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op
return opts.PivotTableStyleName return opts.PivotTableStyleName
} }
pt := xlsxPivotTableDefinition{ pt := xlsxPivotTableDefinition{
Name: fmt.Sprintf("Pivot Table%d", pivotTableID), Name: opts.Name,
CacheID: cacheID, CacheID: cacheID,
RowGrandTotals: &opts.RowGrandTotals, RowGrandTotals: &opts.RowGrandTotals,
ColGrandTotals: &opts.ColGrandTotals, ColGrandTotals: &opts.ColGrandTotals,
UpdatedVersion: pivotTableVersion, UpdatedVersion: pivotTableRefreshedVersion,
MinRefreshableVersion: pivotTableVersion, MinRefreshableVersion: pivotTableVersion,
ShowDrill: &opts.ShowDrill, ShowDrill: &opts.ShowDrill,
UseAutoFormatting: &opts.UseAutoFormatting, UseAutoFormatting: &opts.UseAutoFormatting,
@ -346,10 +356,13 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op
MergeItem: &opts.MergeItem, MergeItem: &opts.MergeItem,
CreatedVersion: pivotTableVersion, CreatedVersion: pivotTableVersion,
CompactData: &opts.CompactData, CompactData: &opts.CompactData,
GridDropZones: opts.ClassicLayout,
ShowError: &opts.ShowError, ShowError: &opts.ShowError,
FieldPrintTitles: opts.FieldPrintTitles,
ItemPrintTitles: opts.ItemPrintTitles,
DataCaption: "Values", DataCaption: "Values",
Location: &xlsxLocation{ Location: &xlsxLocation{
Ref: hCell + ":" + vCell, Ref: topLeftCell + ":" + bottomRightCell,
FirstDataCol: 1, FirstDataCol: 1,
FirstDataRow: 1, FirstDataRow: 1,
FirstHeaderRow: 1, FirstHeaderRow: 1,
@ -376,6 +389,14 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op
ShowLastColumn: opts.ShowLastColumn, ShowLastColumn: opts.ShowLastColumn,
}, },
} }
if pt.Name == "" {
pt.Name = fmt.Sprintf("PivotTable%d", pivotTableID)
}
// set classic layout
if opts.ClassicLayout {
pt.Compact, pt.CompactData = boolPtr(false), boolPtr(false)
}
// pivot fields // pivot fields
_ = f.addPivotFields(&pt, opts) _ = f.addPivotFields(&pt, opts)
@ -390,7 +411,7 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op
_ = f.addPivotDataFields(&pt, opts) _ = f.addPivotDataFields(&pt, opts)
pivotTable, err := xml.Marshal(pt) pivotTable, err := xml.Marshal(pt)
f.saveFileList(pivotTableXML, pivotTable) f.saveFileList(opts.pivotTableXML, pivotTable)
return err return err
} }
@ -454,6 +475,7 @@ func (f *File) addPivotDataFields(pt *xlsxPivotTableDefinition, opts *PivotTable
} }
dataFieldsSubtotals := f.getPivotTableFieldsSubtotal(opts.Data) dataFieldsSubtotals := f.getPivotTableFieldsSubtotal(opts.Data)
dataFieldsName := f.getPivotTableFieldsName(opts.Data) dataFieldsName := f.getPivotTableFieldsName(opts.Data)
dataFieldsNumFmtID := f.getPivotTableFieldsNumFmtID(opts.Data)
for idx, dataField := range dataFieldsIndex { for idx, dataField := range dataFieldsIndex {
if pt.DataFields == nil { if pt.DataFields == nil {
pt.DataFields = &xlsxDataFields{} pt.DataFields = &xlsxDataFields{}
@ -462,6 +484,7 @@ func (f *File) addPivotDataFields(pt *xlsxPivotTableDefinition, opts *PivotTable
Name: dataFieldsName[idx], Name: dataFieldsName[idx],
Fld: dataField, Fld: dataField,
Subtotal: dataFieldsSubtotals[idx], Subtotal: dataFieldsSubtotals[idx],
NumFmtID: dataFieldsNumFmtID[idx],
}) })
} }
@ -525,10 +548,18 @@ func (f *File) addPivotColFields(pt *xlsxPivotTableDefinition, opts *PivotTableO
return err return err
} }
// setClassicLayout provides a method to set classic layout for pivot table by
// setting Compact and Outline to false.
func (fld *xlsxPivotField) setClassicLayout(classicLayout bool) {
if classicLayout {
fld.Compact, fld.Outline = boolPtr(false), boolPtr(false)
}
}
// addPivotFields create pivot fields based on the column order of the first // addPivotFields create pivot fields based on the column order of the first
// row in the data region by given pivot table definition and option. // row in the data region by given pivot table definition and option.
func (f *File) addPivotFields(pt *xlsxPivotTableDefinition, opts *PivotTableOptions) error { func (f *File) addPivotFields(pt *xlsxPivotTableDefinition, opts *PivotTableOptions) error {
order, err := f.getPivotFieldsOrder(opts) order, err := f.getTableFieldsOrder(opts)
if err != nil { if err != nil {
return err return err
} }
@ -542,23 +573,26 @@ func (f *File) addPivotFields(pt *xlsxPivotTableDefinition, opts *PivotTableOpti
} else { } else {
items = append(items, &xlsxItem{T: "default"}) items = append(items, &xlsxItem{T: "default"})
} }
fld := &xlsxPivotField{
pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, &xlsxPivotField{
Name: f.getPivotTableFieldName(name, opts.Rows), Name: f.getPivotTableFieldName(name, opts.Rows),
Axis: "axisRow", Axis: "axisRow",
DataField: inPivotTableField(opts.Data, name) != -1, DataField: inPivotTableField(opts.Data, name) != -1,
Compact: &rowOptions.Compact, Compact: &rowOptions.Compact,
Outline: &rowOptions.Outline, Outline: &rowOptions.Outline,
ShowAll: rowOptions.ShowAll,
InsertBlankRow: rowOptions.InsertBlankRow,
DefaultSubtotal: &rowOptions.DefaultSubtotal, DefaultSubtotal: &rowOptions.DefaultSubtotal,
Items: &xlsxItems{ Items: &xlsxItems{
Count: len(items), Count: len(items),
Item: items, Item: items,
}, },
}) }
fld.setClassicLayout(opts.ClassicLayout)
pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, fld)
continue continue
} }
if inPivotTableField(opts.Filter, name) != -1 { if inPivotTableField(opts.Filter, name) != -1 {
pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, &xlsxPivotField{ fld := &xlsxPivotField{
Axis: "axisPage", Axis: "axisPage",
DataField: inPivotTableField(opts.Data, name) != -1, DataField: inPivotTableField(opts.Data, name) != -1,
Name: f.getPivotTableFieldName(name, opts.Columns), Name: f.getPivotTableFieldName(name, opts.Columns),
@ -568,7 +602,9 @@ func (f *File) addPivotFields(pt *xlsxPivotTableDefinition, opts *PivotTableOpti
{T: "default"}, {T: "default"},
}, },
}, },
}) }
fld.setClassicLayout(opts.ClassicLayout)
pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, fld)
continue continue
} }
if inPivotTableField(opts.Columns, name) != -1 { if inPivotTableField(opts.Columns, name) != -1 {
@ -579,33 +615,41 @@ func (f *File) addPivotFields(pt *xlsxPivotTableDefinition, opts *PivotTableOpti
} else { } else {
items = append(items, &xlsxItem{T: "default"}) items = append(items, &xlsxItem{T: "default"})
} }
pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, &xlsxPivotField{ fld := &xlsxPivotField{
Name: f.getPivotTableFieldName(name, opts.Columns), Name: f.getPivotTableFieldName(name, opts.Columns),
Axis: "axisCol", Axis: "axisCol",
DataField: inPivotTableField(opts.Data, name) != -1, DataField: inPivotTableField(opts.Data, name) != -1,
Compact: &columnOptions.Compact, Compact: &columnOptions.Compact,
Outline: &columnOptions.Outline, Outline: &columnOptions.Outline,
ShowAll: columnOptions.ShowAll,
InsertBlankRow: columnOptions.InsertBlankRow,
DefaultSubtotal: &columnOptions.DefaultSubtotal, DefaultSubtotal: &columnOptions.DefaultSubtotal,
Items: &xlsxItems{ Items: &xlsxItems{
Count: len(items), Count: len(items),
Item: items, Item: items,
}, },
}) }
fld.setClassicLayout(opts.ClassicLayout)
pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, fld)
continue continue
} }
if inPivotTableField(opts.Data, name) != -1 { if inPivotTableField(opts.Data, name) != -1 {
pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, &xlsxPivotField{ fld := &xlsxPivotField{
DataField: true, DataField: true,
}) }
fld.setClassicLayout(opts.ClassicLayout)
pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, fld)
continue continue
} }
pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, &xlsxPivotField{}) fld := &xlsxPivotField{}
fld.setClassicLayout(opts.ClassicLayout)
pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, fld)
} }
return err return err
} }
// countPivotTables provides a function to get drawing files count storage in // countPivotTables provides a function to get pivot table files count storage
// the folder xl/pivotTables. // in the folder xl/pivotTables.
func (f *File) countPivotTables() int { func (f *File) countPivotTables() int {
count := 0 count := 0
f.Pkg.Range(func(k, v interface{}) bool { f.Pkg.Range(func(k, v interface{}) bool {
@ -617,8 +661,8 @@ func (f *File) countPivotTables() int {
return count return count
} }
// countPivotCache provides a function to get drawing files count storage in // countPivotCache provides a function to get pivot table cache definition files
// the folder xl/pivotCache. // count storage in the folder xl/pivotCache.
func (f *File) countPivotCache() int { func (f *File) countPivotCache() int {
count := 0 count := 0
f.Pkg.Range(func(k, v interface{}) bool { f.Pkg.Range(func(k, v interface{}) bool {
@ -634,7 +678,7 @@ func (f *File) countPivotCache() int {
// to a sequential index by given fields and pivot option. // to a sequential index by given fields and pivot option.
func (f *File) getPivotFieldsIndex(fields []PivotTableField, opts *PivotTableOptions) ([]int, error) { func (f *File) getPivotFieldsIndex(fields []PivotTableField, opts *PivotTableOptions) ([]int, error) {
var pivotFieldsIndex []int var pivotFieldsIndex []int
orders, err := f.getPivotFieldsOrder(opts) orders, err := f.getTableFieldsOrder(opts)
if err != nil { if err != nil {
return pivotFieldsIndex, err return pivotFieldsIndex, err
} }
@ -689,6 +733,22 @@ func (f *File) getPivotTableFieldName(name string, fields []PivotTableField) str
return "" return ""
} }
// getPivotTableFieldsNumFmtID prepare fields number format ID by given pivot
// table fields.
func (f *File) getPivotTableFieldsNumFmtID(fields []PivotTableField) []int {
field := make([]int, len(fields))
for idx, fld := range fields {
if _, ok := builtInNumFmt[fld.NumFmt]; ok {
field[idx] = fld.NumFmt
continue
}
if (27 <= fld.NumFmt && fld.NumFmt <= 36) || (50 <= fld.NumFmt && fld.NumFmt <= 81) {
field[idx] = fld.NumFmt
}
}
return field
}
// getPivotTableFieldOptions return options for specific field by given field name. // getPivotTableFieldOptions return options for specific field by given field name.
func (f *File) getPivotTableFieldOptions(name string, fields []PivotTableField) (options PivotTableField, ok bool) { func (f *File) getPivotTableFieldOptions(name string, fields []PivotTableField) (options PivotTableField, ok bool) {
for _, field := range fields { for _, field := range fields {
@ -719,3 +779,318 @@ func (f *File) addWorkbookPivotCache(RID int) int {
}) })
return cacheID return cacheID
} }
// GetPivotTables returns all pivot table definitions in a worksheet by given
// worksheet name.
func (f *File) GetPivotTables(sheet string) ([]PivotTableOptions, error) {
var pivotTables []PivotTableOptions
name, ok := f.getSheetXMLPath(sheet)
if !ok {
return pivotTables, ErrSheetNotExist{sheet}
}
rels := "xl/worksheets/_rels/" + strings.TrimPrefix(name, "xl/worksheets/") + ".rels"
sheetRels, err := f.relsReader(rels)
if err != nil {
return pivotTables, err
}
if sheetRels == nil {
sheetRels = &xlsxRelationships{}
}
for _, v := range sheetRels.Relationships {
if v.Type == SourceRelationshipPivotTable {
pivotTableXML := strings.ReplaceAll(v.Target, "..", "xl")
pivotCacheRels := "xl/pivotTables/_rels/" + filepath.Base(v.Target) + ".rels"
pivotTable, err := f.getPivotTable(sheet, pivotTableXML, pivotCacheRels)
if err != nil {
return pivotTables, err
}
pivotTables = append(pivotTables, pivotTable)
}
}
return pivotTables, nil
}
// getPivotTableDataRange checking given if data range is a cell reference or
// named reference (defined name or table name), and set pivot table data range.
func (f *File) getPivotTableDataRange(opts *PivotTableOptions) error {
if opts.DataRange == "" {
return newPivotTableDataRangeError(ErrParameterRequired.Error())
}
if opts.pivotDataRange != "" {
return nil
}
if strings.Contains(opts.DataRange, "!") {
opts.pivotDataRange = opts.DataRange
return nil
}
tbls, err := f.getTables()
if err != nil {
return err
}
for sheetName, tables := range tbls {
for _, table := range tables {
if table.Name == opts.DataRange {
opts.pivotDataRange, opts.namedDataRange = fmt.Sprintf("%s!%s", sheetName, table.Range), true
return err
}
}
}
if !opts.namedDataRange {
opts.pivotDataRange = f.getDefinedNameRefTo(opts.DataRange, opts.pivotSheetName)
if opts.pivotDataRange != "" {
opts.namedDataRange = true
return nil
}
}
return newPivotTableDataRangeError(ErrParameterInvalid.Error())
}
// getPivotTable provides a function to get a pivot table definition by given
// worksheet name, pivot table XML path and pivot cache relationship XML path.
func (f *File) getPivotTable(sheet, pivotTableXML, pivotCacheRels string) (PivotTableOptions, error) {
var opts PivotTableOptions
rels, err := f.relsReader(pivotCacheRels)
if err != nil {
return opts, err
}
var pivotCacheXML string
for _, v := range rels.Relationships {
if v.Type == SourceRelationshipPivotCache {
pivotCacheXML = strings.ReplaceAll(v.Target, "..", "xl")
break
}
}
pc, err := f.pivotCacheReader(pivotCacheXML)
if err != nil {
return opts, err
}
pt, err := f.pivotTableReader(pivotTableXML)
if err != nil {
return opts, err
}
opts = PivotTableOptions{
pivotTableXML: pivotTableXML,
pivotCacheXML: pivotCacheXML,
pivotSheetName: sheet,
DataRange: fmt.Sprintf("%s!%s", pc.CacheSource.WorksheetSource.Sheet, pc.CacheSource.WorksheetSource.Ref),
PivotTableRange: fmt.Sprintf("%s!%s", sheet, pt.Location.Ref),
Name: pt.Name,
ClassicLayout: pt.GridDropZones,
FieldPrintTitles: pt.FieldPrintTitles,
ItemPrintTitles: pt.ItemPrintTitles,
}
if pc.CacheSource.WorksheetSource.Name != "" {
opts.DataRange = pc.CacheSource.WorksheetSource.Name
_ = f.getPivotTableDataRange(&opts)
}
fields := []string{"RowGrandTotals", "ColGrandTotals", "ShowDrill", "UseAutoFormatting", "PageOverThenDown", "MergeItem", "CompactData", "ShowError"}
immutable, mutable := reflect.ValueOf(*pt), reflect.ValueOf(&opts).Elem()
for _, field := range fields {
immutableField := immutable.FieldByName(field)
if immutableField.Kind() == reflect.Ptr && !immutableField.IsNil() && immutableField.Elem().Kind() == reflect.Bool {
mutable.FieldByName(field).SetBool(immutableField.Elem().Bool())
}
}
if si := pt.PivotTableStyleInfo; si != nil {
opts.ShowRowHeaders = si.ShowRowHeaders
opts.ShowColHeaders = si.ShowColHeaders
opts.ShowRowStripes = si.ShowRowStripes
opts.ShowColStripes = si.ShowColStripes
opts.ShowLastColumn = si.ShowLastColumn
opts.PivotTableStyleName = si.Name
}
order, err := f.getTableFieldsOrder(&opts)
if err != nil {
return opts, err
}
f.extractPivotTableFields(order, pt, &opts)
return opts, err
}
// pivotTableReader provides a function to get the pointer to the structure
// after deserialization of xl/pivotTables/pivotTable%d.xml.
func (f *File) pivotTableReader(path string) (*xlsxPivotTableDefinition, error) {
content, ok := f.Pkg.Load(path)
pivotTable := &xlsxPivotTableDefinition{}
if ok && content != nil {
if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(content.([]byte)))).
Decode(pivotTable); err != nil && err != io.EOF {
return nil, err
}
}
return pivotTable, nil
}
// pivotCacheReader provides a function to get the pointer to the structure
// after deserialization of xl/pivotCache/pivotCacheDefinition%d.xml.
func (f *File) pivotCacheReader(path string) (*xlsxPivotCacheDefinition, error) {
content, ok := f.Pkg.Load(path)
pivotCache := &xlsxPivotCacheDefinition{}
if ok && content != nil {
if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(content.([]byte)))).
Decode(pivotCache); err != nil && err != io.EOF {
return nil, err
}
}
return pivotCache, nil
}
// extractPivotTableFields provides a function to extract all pivot table fields
// settings by given pivot table fields.
func (f *File) extractPivotTableFields(order []string, pt *xlsxPivotTableDefinition, opts *PivotTableOptions) {
for fieldIdx, field := range pt.PivotFields.PivotField {
if field.Axis == "axisRow" {
opts.Rows = append(opts.Rows, extractPivotTableField(order[fieldIdx], field))
}
if field.Axis == "axisCol" {
opts.Columns = append(opts.Columns, extractPivotTableField(order[fieldIdx], field))
}
if field.Axis == "axisPage" {
opts.Filter = append(opts.Filter, extractPivotTableField(order[fieldIdx], field))
}
}
if pt.DataFields != nil {
for _, field := range pt.DataFields.DataField {
opts.Data = append(opts.Data, PivotTableField{
Data: order[field.Fld],
Name: field.Name,
Subtotal: cases.Title(language.English).String(field.Subtotal),
NumFmt: field.NumFmtID,
})
}
}
}
// extractPivotTableField provides a function to extract pivot table field
// settings by given pivot table fields.
func extractPivotTableField(data string, fld *xlsxPivotField) PivotTableField {
pivotTableField := PivotTableField{
Data: data,
ShowAll: fld.ShowAll,
InsertBlankRow: fld.InsertBlankRow,
}
fields := []string{"Compact", "Name", "Outline", "Subtotal", "DefaultSubtotal"}
immutable, mutable := reflect.ValueOf(*fld), reflect.ValueOf(&pivotTableField).Elem()
for _, field := range fields {
immutableField := immutable.FieldByName(field)
if immutableField.Kind() == reflect.String {
mutable.FieldByName(field).SetString(immutableField.String())
}
if immutableField.Kind() == reflect.Ptr && !immutableField.IsNil() && immutableField.Elem().Kind() == reflect.Bool {
mutable.FieldByName(field).SetBool(immutableField.Elem().Bool())
}
}
return pivotTableField
}
// genPivotCacheDefinitionID generates a unique pivot table cache definition ID.
func (f *File) genPivotCacheDefinitionID() int {
var (
ID int
decodeExtLst = new(decodeExtLst)
decodeX14PivotCacheDefinition = new(decodeX14PivotCacheDefinition)
)
f.Pkg.Range(func(k, v interface{}) bool {
if strings.Contains(k.(string), "xl/pivotCache/pivotCacheDefinition") {
pc, err := f.pivotCacheReader(k.(string))
if err != nil {
return true
}
if pc.ExtLst != nil {
_ = f.xmlNewDecoder(strings.NewReader("<extLst>" + pc.ExtLst.Ext + "</extLst>")).Decode(decodeExtLst)
for _, ext := range decodeExtLst.Ext {
if ext.URI == ExtURIPivotCacheDefinition {
_ = f.xmlNewDecoder(strings.NewReader(ext.Content)).Decode(decodeX14PivotCacheDefinition)
if ID < decodeX14PivotCacheDefinition.PivotCacheID {
ID = decodeX14PivotCacheDefinition.PivotCacheID
}
}
}
}
}
return true
})
return ID + 1
}
// deleteWorkbookPivotCache remove workbook pivot cache and pivot cache
// relationships.
func (f *File) deleteWorkbookPivotCache(opt PivotTableOptions) error {
rID, err := f.deleteWorkbookRels(SourceRelationshipPivotCache, strings.TrimPrefix(strings.TrimPrefix(opt.pivotCacheXML, "/"), "xl/"))
if err != nil {
return err
}
wb, err := f.workbookReader()
if err != nil {
return err
}
if wb.PivotCaches != nil {
for i, pivotCache := range wb.PivotCaches.PivotCache {
if pivotCache.RID == rID {
wb.PivotCaches.PivotCache = append(wb.PivotCaches.PivotCache[:i], wb.PivotCaches.PivotCache[i+1:]...)
}
}
if len(wb.PivotCaches.PivotCache) == 0 {
wb.PivotCaches = nil
}
}
return err
}
// DeletePivotTable delete a pivot table by giving the worksheet name and pivot
// table name. Note that this function does not clean cell values in the pivot
// table range.
func (f *File) DeletePivotTable(sheet, name string) error {
sheetXML, ok := f.getSheetXMLPath(sheet)
if !ok {
return ErrSheetNotExist{sheet}
}
rels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetXML, "xl/worksheets/") + ".rels"
sheetRels, err := f.relsReader(rels)
if err != nil {
return err
}
if sheetRels == nil {
sheetRels = &xlsxRelationships{}
}
opts, err := f.GetPivotTables(sheet)
if err != nil {
return err
}
pivotTableCaches := map[string]int{}
pivotTables, _ := f.getPivotTables()
for _, sheetPivotTables := range pivotTables {
for _, sheetPivotTable := range sheetPivotTables {
pivotTableCaches[sheetPivotTable.pivotCacheXML]++
}
}
for _, v := range sheetRels.Relationships {
for _, opt := range opts {
if v.Type == SourceRelationshipPivotTable {
pivotTableXML := strings.ReplaceAll(v.Target, "..", "xl")
if opt.Name == name && opt.pivotTableXML == pivotTableXML {
if pivotTableCaches[opt.pivotCacheXML] == 1 {
err = f.deleteWorkbookPivotCache(opt)
}
f.deleteSheetRelationships(sheet, v.ID)
return err
}
}
}
}
return newNoExistTableError(name)
}
// getPivotTables provides a function to get all pivot tables in a workbook.
func (f *File) getPivotTables() (map[string][]PivotTableOptions, error) {
pivotTables := map[string][]PivotTableOptions{}
for _, sheetName := range f.GetSheetList() {
pts, err := f.GetPivotTables(sheetName)
e := ErrSheetNotExist{sheetName}
if err != nil && err.Error() != newNotWorksheetError(sheetName).Error() && err.Error() != e.Error() {
return pivotTables, err
}
pivotTables[sheetName] = append(pivotTables[sheetName], pts...)
}
return pivotTables, nil
}

View File

@ -10,7 +10,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestAddPivotTable(t *testing.T) { func TestPivotTable(t *testing.T) {
f := NewFile() f := NewFile()
// Create some data in a sheet // Create some data in a sheet
month := []string{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"} month := []string{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}
@ -25,25 +25,38 @@ func TestAddPivotTable(t *testing.T) {
assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("D%d", row), rand.Intn(5000))) assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("D%d", row), rand.Intn(5000)))
assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("E%d", row), region[rand.Intn(4)])) assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("E%d", row), region[rand.Intn(4)]))
} }
assert.NoError(t, f.AddPivotTable(&PivotTableOptions{ expected := &PivotTableOptions{
DataRange: "Sheet1!$A$1:$E$31", pivotTableXML: "xl/pivotTables/pivotTable1.xml",
PivotTableRange: "Sheet1!$G$2:$M$34", pivotCacheXML: "xl/pivotCache/pivotCacheDefinition1.xml",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, DataRange: "Sheet1!A1:E31",
Filter: []PivotTableField{{Data: "Region"}}, PivotTableRange: "Sheet1!G2:M34",
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, Name: "PivotTable1",
Data: []PivotTableField{{Data: "Sales", Subtotal: "Sum", Name: "Summarize by Sum"}}, Rows: []PivotTableField{{Data: "Month", ShowAll: true, DefaultSubtotal: true}, {Data: "Year"}},
RowGrandTotals: true, Filter: []PivotTableField{{Data: "Region"}},
ColGrandTotals: true, Columns: []PivotTableField{{Data: "Type", ShowAll: true, InsertBlankRow: true, DefaultSubtotal: true}},
ShowDrill: true, Data: []PivotTableField{{Data: "Sales", Subtotal: "Sum", Name: "Summarize by Sum", NumFmt: 38}},
ShowRowHeaders: true, RowGrandTotals: true,
ShowColHeaders: true, ColGrandTotals: true,
ShowLastColumn: true, ShowDrill: true,
ShowError: true, ClassicLayout: true,
})) ShowError: true,
ShowRowHeaders: true,
ShowColHeaders: true,
ShowLastColumn: true,
FieldPrintTitles: true,
ItemPrintTitles: true,
PivotTableStyleName: "PivotStyleLight16",
}
assert.NoError(t, f.AddPivotTable(expected))
// Test get pivot table
pivotTables, err := f.GetPivotTables("Sheet1")
assert.NoError(t, err)
assert.Len(t, pivotTables, 1)
assert.Equal(t, *expected, pivotTables[0])
// Use different order of coordinate tests // Use different order of coordinate tests
assert.NoError(t, f.AddPivotTable(&PivotTableOptions{ assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!$A$1:$E$31", DataRange: "Sheet1!A1:E31",
PivotTableRange: "Sheet1!$U$34:$O$2", PivotTableRange: "Sheet1!U34:O2",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
Data: []PivotTableField{{Data: "Sales", Subtotal: "Average", Name: "Summarize by Average"}}, Data: []PivotTableField{{Data: "Sales", Subtotal: "Average", Name: "Summarize by Average"}},
@ -54,10 +67,15 @@ func TestAddPivotTable(t *testing.T) {
ShowColHeaders: true, ShowColHeaders: true,
ShowLastColumn: true, ShowLastColumn: true,
})) }))
// Test get pivot table with default style name
pivotTables, err = f.GetPivotTables("Sheet1")
assert.NoError(t, err)
assert.Len(t, pivotTables, 2)
assert.Equal(t, "PivotStyleLight16", pivotTables[1].PivotTableStyleName)
assert.NoError(t, f.AddPivotTable(&PivotTableOptions{ assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!$A$1:$E$31", DataRange: "Sheet1!A1:E31",
PivotTableRange: "Sheet1!$W$2:$AC$34", PivotTableRange: "Sheet1!W2:AC34",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Columns: []PivotTableField{{Data: "Region"}}, Columns: []PivotTableField{{Data: "Region"}},
Data: []PivotTableField{{Data: "Sales", Subtotal: "Count", Name: "Summarize by Count"}}, Data: []PivotTableField{{Data: "Sales", Subtotal: "Count", Name: "Summarize by Count"}},
@ -69,8 +87,8 @@ func TestAddPivotTable(t *testing.T) {
ShowLastColumn: true, ShowLastColumn: true,
})) }))
assert.NoError(t, f.AddPivotTable(&PivotTableOptions{ assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!$A$1:$E$31", DataRange: "Sheet1!A1:E31",
PivotTableRange: "Sheet1!$G$42:$W$55", PivotTableRange: "Sheet1!G42:W55",
Rows: []PivotTableField{{Data: "Month"}}, Rows: []PivotTableField{{Data: "Month"}},
Columns: []PivotTableField{{Data: "Region", DefaultSubtotal: true}, {Data: "Year"}}, Columns: []PivotTableField{{Data: "Region", DefaultSubtotal: true}, {Data: "Year"}},
Data: []PivotTableField{{Data: "Sales", Subtotal: "CountNums", Name: "Summarize by CountNums"}}, Data: []PivotTableField{{Data: "Sales", Subtotal: "CountNums", Name: "Summarize by CountNums"}},
@ -82,8 +100,8 @@ func TestAddPivotTable(t *testing.T) {
ShowLastColumn: true, ShowLastColumn: true,
})) }))
assert.NoError(t, f.AddPivotTable(&PivotTableOptions{ assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!$A$1:$E$31", DataRange: "Sheet1!A1:E31",
PivotTableRange: "Sheet1!$AE$2:$AG$33", PivotTableRange: "Sheet1!AE2:AG33",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Data: []PivotTableField{{Data: "Sales", Subtotal: "Max", Name: "Summarize by Max"}, {Data: "Sales", Subtotal: "Average", Name: "Average of Sales"}}, Data: []PivotTableField{{Data: "Sales", Subtotal: "Max", Name: "Summarize by Max"}, {Data: "Sales", Subtotal: "Average", Name: "Average of Sales"}},
RowGrandTotals: true, RowGrandTotals: true,
@ -95,8 +113,8 @@ func TestAddPivotTable(t *testing.T) {
})) }))
// Create pivot table with empty subtotal field name and specified style // Create pivot table with empty subtotal field name and specified style
assert.NoError(t, f.AddPivotTable(&PivotTableOptions{ assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!$A$1:$E$31", DataRange: "Sheet1!A1:E31",
PivotTableRange: "Sheet1!$AJ$2:$AP1$35", PivotTableRange: "Sheet1!AJ2:AP135",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Filter: []PivotTableField{{Data: "Region"}}, Filter: []PivotTableField{{Data: "Region"}},
Columns: []PivotTableField{}, Columns: []PivotTableField{},
@ -109,14 +127,14 @@ func TestAddPivotTable(t *testing.T) {
ShowLastColumn: true, ShowLastColumn: true,
PivotTableStyleName: "PivotStyleLight19", PivotTableStyleName: "PivotStyleLight19",
})) }))
_, err := f.NewSheet("Sheet2") _, err = f.NewSheet("Sheet2")
assert.NoError(t, err) assert.NoError(t, err)
assert.NoError(t, f.AddPivotTable(&PivotTableOptions{ assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!$A$1:$E$31", DataRange: "Sheet1!A1:E31",
PivotTableRange: "Sheet2!$A$1:$AR$15", PivotTableRange: "Sheet2!A1:AN17",
Rows: []PivotTableField{{Data: "Month"}}, Rows: []PivotTableField{{Data: "Month"}},
Columns: []PivotTableField{{Data: "Region", DefaultSubtotal: true}, {Data: "Type", DefaultSubtotal: true}, {Data: "Year"}}, Columns: []PivotTableField{{Data: "Region", DefaultSubtotal: true}, {Data: "Type", DefaultSubtotal: true}, {Data: "Year"}},
Data: []PivotTableField{{Data: "Sales", Subtotal: "Min", Name: "Summarize by Min"}}, Data: []PivotTableField{{Data: "Sales", Subtotal: "Min", Name: "Summarize by Min", NumFmt: 32}},
RowGrandTotals: true, RowGrandTotals: true,
ColGrandTotals: true, ColGrandTotals: true,
ShowDrill: true, ShowDrill: true,
@ -124,12 +142,19 @@ func TestAddPivotTable(t *testing.T) {
ShowColHeaders: true, ShowColHeaders: true,
ShowLastColumn: true, ShowLastColumn: true,
})) }))
// Test get pivot table with across worksheet data range
pivotTables, err = f.GetPivotTables("Sheet2")
assert.NoError(t, err)
assert.Len(t, pivotTables, 1)
assert.Equal(t, "Sheet1!A1:E31", pivotTables[0].DataRange)
assert.NoError(t, f.AddPivotTable(&PivotTableOptions{ assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!$A$1:$E$31", DataRange: "Sheet1!A1:E31",
PivotTableRange: "Sheet2!$A$18:$AR$54", PivotTableRange: "Sheet2!A20:AR60",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Type"}}, Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Type"}},
Columns: []PivotTableField{{Data: "Region", DefaultSubtotal: true}, {Data: "Year"}}, Columns: []PivotTableField{{Data: "Region", DefaultSubtotal: true}, {Data: "Year"}},
Data: []PivotTableField{{Data: "Sales", Subtotal: "Product", Name: "Summarize by Product"}}, Data: []PivotTableField{{Data: "Sales", Subtotal: "Product", Name: "Summarize by Product", NumFmt: 32}},
RowGrandTotals: true, RowGrandTotals: true,
ColGrandTotals: true, ColGrandTotals: true,
ShowDrill: true, ShowDrill: true,
@ -140,16 +165,16 @@ func TestAddPivotTable(t *testing.T) {
// Create pivot table with many data, many rows, many cols and defined name // Create pivot table with many data, many rows, many cols and defined name
assert.NoError(t, f.SetDefinedName(&DefinedName{ assert.NoError(t, f.SetDefinedName(&DefinedName{
Name: "dataRange", Name: "dataRange",
RefersTo: "Sheet1!$A$1:$E$31", RefersTo: "Sheet1!A1:E31",
Comment: "Pivot Table Data Range", Comment: "Pivot Table Data Range",
Scope: "Sheet2", Scope: "Sheet2",
})) }))
assert.NoError(t, f.AddPivotTable(&PivotTableOptions{ assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "dataRange", DataRange: "dataRange",
PivotTableRange: "Sheet2!$A$57:$AJ$91", PivotTableRange: "Sheet2!A65:AJ100",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Columns: []PivotTableField{{Data: "Region", DefaultSubtotal: true}, {Data: "Type"}}, Columns: []PivotTableField{{Data: "Region", DefaultSubtotal: true}, {Data: "Type"}},
Data: []PivotTableField{{Data: "Sales", Subtotal: "Sum", Name: "Sum of Sales"}, {Data: "Sales", Subtotal: "Average", Name: "Average of Sales"}}, Data: []PivotTableField{{Data: "Sales", Subtotal: "Sum", Name: "Sum of Sales", NumFmt: -1}, {Data: "Sales", Subtotal: "Average", Name: "Average of Sales", NumFmt: 38}},
RowGrandTotals: true, RowGrandTotals: true,
ColGrandTotals: true, ColGrandTotals: true,
ShowDrill: true, ShowDrill: true,
@ -159,106 +184,123 @@ func TestAddPivotTable(t *testing.T) {
})) }))
// Test empty pivot table options // Test empty pivot table options
assert.EqualError(t, f.AddPivotTable(nil), ErrParameterRequired.Error()) assert.Equal(t, ErrParameterRequired, f.AddPivotTable(nil))
// Test add pivot table with custom name which exceeds the max characters limit
assert.Equal(t, ErrNameLength, f.AddPivotTable(&PivotTableOptions{
DataRange: "dataRange",
PivotTableRange: "Sheet2!A65:AJ100",
Name: strings.Repeat("c", MaxFieldLength+1),
}))
// Test invalid data range // Test invalid data range
assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{ assert.Equal(t, newPivotTableDataRangeError("parameter is invalid"), f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!$A$1:$A$1", DataRange: "Sheet1!A1:A1",
PivotTableRange: "Sheet1!$U$34:$O$2", PivotTableRange: "Sheet1!U34:O2",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
Data: []PivotTableField{{Data: "Sales"}}, Data: []PivotTableField{{Data: "Sales"}},
}), `parameter 'DataRange' parsing error: parameter is invalid`) }))
// Test the data range of the worksheet that is not declared // Test the data range of the worksheet that is not declared
assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{ assert.Equal(t, newPivotTableDataRangeError("parameter is invalid"), f.AddPivotTable(&PivotTableOptions{
DataRange: "$A$1:$E$31", DataRange: "A1:E31",
PivotTableRange: "Sheet1!$U$34:$O$2", PivotTableRange: "Sheet1!U34:O2",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
Data: []PivotTableField{{Data: "Sales"}}, Data: []PivotTableField{{Data: "Sales"}},
}), `parameter 'DataRange' parsing error: parameter is invalid`) }))
// Test the worksheet declared in the data range does not exist // Test the worksheet declared in the data range does not exist
assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{ assert.Equal(t, ErrSheetNotExist{"SheetN"}, f.AddPivotTable(&PivotTableOptions{
DataRange: "SheetN!$A$1:$E$31", DataRange: "SheetN!A1:E31",
PivotTableRange: "Sheet1!$U$34:$O$2", PivotTableRange: "Sheet1!U34:O2",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
Data: []PivotTableField{{Data: "Sales"}}, Data: []PivotTableField{{Data: "Sales"}},
}), "sheet SheetN does not exist") }))
// Test the pivot table range of the worksheet that is not declared // Test the pivot table range of the worksheet that is not declared
assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{ assert.Equal(t, newPivotTableRangeError("parameter is invalid"), f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!$A$1:$E$31", DataRange: "Sheet1!A1:E31",
PivotTableRange: "$U$34:$O$2", PivotTableRange: "U34:O2",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
Data: []PivotTableField{{Data: "Sales"}}, Data: []PivotTableField{{Data: "Sales"}},
}), `parameter 'PivotTableRange' parsing error: parameter is invalid`) }))
// Test the worksheet declared in the pivot table range does not exist // Test the worksheet declared in the pivot table range does not exist
assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{ assert.Equal(t, ErrSheetNotExist{"SheetN"}, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!$A$1:$E$31", DataRange: "Sheet1!A1:E31",
PivotTableRange: "SheetN!$U$34:$O$2", PivotTableRange: "SheetN!U34:O2",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
Data: []PivotTableField{{Data: "Sales"}}, Data: []PivotTableField{{Data: "Sales"}},
}), "sheet SheetN does not exist") }))
// Test not exists worksheet in data range // Test not exists worksheet in data range
assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{ assert.Equal(t, ErrSheetNotExist{"SheetN"}, f.AddPivotTable(&PivotTableOptions{
DataRange: "SheetN!$A$1:$E$31", DataRange: "SheetN!A1:E31",
PivotTableRange: "Sheet1!$U$34:$O$2", PivotTableRange: "Sheet1!U34:O2",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
Data: []PivotTableField{{Data: "Sales"}}, Data: []PivotTableField{{Data: "Sales"}},
}), "sheet SheetN does not exist") }))
// Test invalid row number in data range // Test invalid row number in data range
assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{ assert.Equal(t, newPivotTableDataRangeError(newCellNameToCoordinatesError("A0", newInvalidCellNameError("A0")).Error()), f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!$A$0:$E$31", DataRange: "Sheet1!A0:E31",
PivotTableRange: "Sheet1!$U$34:$O$2", PivotTableRange: "Sheet1!U34:O2",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
Data: []PivotTableField{{Data: "Sales"}}, Data: []PivotTableField{{Data: "Sales"}},
}), `parameter 'DataRange' parsing error: cannot convert cell "A0" to coordinates: invalid cell name "A0"`) }))
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddPivotTable1.xlsx"))) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddPivotTable1.xlsx")))
// Test with field names that exceed the length limit and invalid subtotal // Test with field names that exceed the length limit and invalid subtotal
assert.NoError(t, f.AddPivotTable(&PivotTableOptions{ assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!$A$1:$E$31", DataRange: "Sheet1!A1:E31",
PivotTableRange: "Sheet1!$G$2:$M$34", PivotTableRange: "Sheet1!G2:M34",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
Data: []PivotTableField{{Data: "Sales", Subtotal: "-", Name: strings.Repeat("s", MaxFieldLength+1)}}, Data: []PivotTableField{{Data: "Sales", Subtotal: "-", Name: strings.Repeat("s", MaxFieldLength+1)}},
})) }))
// Test delete pivot table
pivotTables, err = f.GetPivotTables("Sheet1")
assert.Len(t, pivotTables, 7)
assert.NoError(t, err)
assert.NoError(t, f.DeletePivotTable("Sheet1", "PivotTable1"))
pivotTables, err = f.GetPivotTables("Sheet1")
assert.Len(t, pivotTables, 6)
assert.NoError(t, err)
// Test add pivot table with invalid sheet name // Test add pivot table with invalid sheet name
assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{ assert.Error(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet:1!$A$1:$E$31", DataRange: "Sheet:1!A1:E31",
PivotTableRange: "Sheet:1!$G$2:$M$34", PivotTableRange: "Sheet:1!G2:M34",
Rows: []PivotTableField{{Data: "Year"}}, Rows: []PivotTableField{{Data: "Year"}},
}), ErrSheetNameInvalid.Error()) }), ErrSheetNameInvalid)
// Test add pivot table with enable ClassicLayout and CompactData in the same time
assert.Error(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!A1:E31",
PivotTableRange: "Sheet1!G2:M34",
CompactData: true,
ClassicLayout: true,
}), ErrPivotTableClassicLayout)
// Test delete pivot table with not exists worksheet
assert.EqualError(t, f.DeletePivotTable("SheetN", "PivotTable1"), "sheet SheetN does not exist")
// Test delete pivot table with not exists pivot table name
assert.EqualError(t, f.DeletePivotTable("Sheet1", "PivotTableN"), "table PivotTableN does not exist")
// Test adjust range with invalid range // Test adjust range with invalid range
_, _, err = f.adjustRange("") _, _, err = f.adjustRange("")
assert.EqualError(t, err, ErrParameterRequired.Error()) assert.Error(t, err, ErrParameterRequired)
// Test adjust range with incorrect range // Test adjust range with incorrect range
_, _, err = f.adjustRange("sheet1!") _, _, err = f.adjustRange("sheet1!")
assert.EqualError(t, err, "parameter is invalid") assert.EqualError(t, err, "parameter is invalid")
// Test get pivot fields order with empty data range // Test get table fields order with empty data range
_, err = f.getPivotFieldsOrder(&PivotTableOptions{}) _, err = f.getTableFieldsOrder(&PivotTableOptions{})
assert.EqualError(t, err, `parameter 'DataRange' parsing error: parameter is required`) assert.EqualError(t, err, `parameter 'DataRange' parsing error: parameter is required`)
// Test add pivot cache with empty data range // Test add pivot cache with empty data range
assert.EqualError(t, f.addPivotCache("", &PivotTableOptions{}), "parameter 'DataRange' parsing error: parameter is required") assert.EqualError(t, f.addPivotCache(&PivotTableOptions{}), "parameter 'DataRange' parsing error: parameter is required")
// Test add pivot cache with invalid data range
assert.EqualError(t, f.addPivotCache("", &PivotTableOptions{
DataRange: "$A$1:$E$31",
PivotTableRange: "Sheet1!$U$34:$O$2",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
Data: []PivotTableField{{Data: "Sales"}},
}), "parameter 'DataRange' parsing error: parameter is invalid")
// Test add pivot table with empty options // Test add pivot table with empty options
assert.EqualError(t, f.addPivotTable(0, 0, "", &PivotTableOptions{}), "parameter 'PivotTableRange' parsing error: parameter is required") assert.EqualError(t, f.addPivotTable(0, 0, &PivotTableOptions{}), "parameter 'PivotTableRange' parsing error: parameter is required")
// Test add pivot table with invalid data range // Test add pivot table with invalid data range
assert.EqualError(t, f.addPivotTable(0, 0, "", &PivotTableOptions{}), "parameter 'PivotTableRange' parsing error: parameter is required") assert.EqualError(t, f.addPivotTable(0, 0, &PivotTableOptions{}), "parameter 'PivotTableRange' parsing error: parameter is required")
// Test add pivot fields with empty data range // Test add pivot fields with empty data range
assert.EqualError(t, f.addPivotFields(nil, &PivotTableOptions{ assert.EqualError(t, f.addPivotFields(nil, &PivotTableOptions{
DataRange: "$A$1:$E$31", DataRange: "A1:E31",
PivotTableRange: "Sheet1!$U$34:$O$2", PivotTableRange: "Sheet1!U34:O2",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}}, Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
Data: []PivotTableField{{Data: "Sales"}}, Data: []PivotTableField{{Data: "Sales"}},
@ -268,20 +310,145 @@ func TestAddPivotTable(t *testing.T) {
assert.EqualError(t, err, `parameter 'DataRange' parsing error: parameter is required`) assert.EqualError(t, err, `parameter 'DataRange' parsing error: parameter is required`)
// Test add pivot table with unsupported charset content types. // Test add pivot table with unsupported charset content types.
f = NewFile() f = NewFile()
assert.NoError(t, f.SetSheetRow("Sheet1", "A1", &[]string{"Month", "Year", "Type", "Sales", "Region"}))
f.ContentTypes = nil f.ContentTypes = nil
f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{ assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!$A$1:$E$31", DataRange: "Sheet1!A1:E31",
PivotTableRange: "Sheet1!$G$2:$M$34", PivotTableRange: "Sheet1!G2:M34",
Rows: []PivotTableField{{Data: "Year"}}, Rows: []PivotTableField{{Data: "Year"}},
}), "XML syntax error on line 1: invalid UTF-8") }), "XML syntax error on line 1: invalid UTF-8")
assert.NoError(t, f.Close())
// Test get pivot table without pivot table
f = NewFile()
pivotTables, err = f.GetPivotTables("Sheet1")
assert.NoError(t, err)
assert.Len(t, pivotTables, 0)
// Test get pivot table with not exists worksheet
_, err = f.GetPivotTables("SheetN")
assert.EqualError(t, err, "sheet SheetN does not exist")
// Test get pivot table with unsupported charset worksheet relationships
f.Pkg.Store("xl/worksheets/_rels/sheet1.xml.rels", MacintoshCyrillicCharset)
_, err = f.GetPivotTables("Sheet1")
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
assert.NoError(t, f.Close())
// Test get pivot table with unsupported charset pivot cache definition
f, err = OpenFile(filepath.Join("test", "TestAddPivotTable1.xlsx"))
assert.NoError(t, err)
f.Pkg.Store("xl/pivotCache/pivotCacheDefinition1.xml", MacintoshCyrillicCharset)
_, err = f.GetPivotTables("Sheet1")
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
assert.NoError(t, f.Close())
// Test get pivot table with unsupported charset pivot table relationships
f, err = OpenFile(filepath.Join("test", "TestAddPivotTable1.xlsx"))
assert.NoError(t, err)
f.Pkg.Store("xl/pivotTables/_rels/pivotTable1.xml.rels", MacintoshCyrillicCharset)
_, err = f.GetPivotTables("Sheet1")
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
assert.NoError(t, f.Close())
// Test get pivot table with unsupported charset pivot table
f, err = OpenFile(filepath.Join("test", "TestAddPivotTable1.xlsx"))
assert.NoError(t, err)
f.Pkg.Store("xl/pivotTables/pivotTable1.xml", MacintoshCyrillicCharset)
_, err = f.GetPivotTables("Sheet1")
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
_, err = f.getPivotTables()
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
assert.NoError(t, f.Close())
}
func TestPivotTableDataRange(t *testing.T) {
f := NewFile()
// Create table in a worksheet
assert.NoError(t, f.AddTable("Sheet1", &Table{
Name: "Table1",
Range: "A1:D5",
}))
for row := 2; row < 6; row++ {
assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("A%d", row), rand.Intn(10)))
assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("B%d", row), rand.Intn(10)))
assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("C%d", row), rand.Intn(10)))
assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("D%d", row), rand.Intn(10)))
}
// Test add pivot table with table data range
opts := PivotTableOptions{
DataRange: "Table1",
PivotTableRange: "Sheet1!G2:K7",
Rows: []PivotTableField{{Data: "Column1"}},
Columns: []PivotTableField{{Data: "Column2"}},
RowGrandTotals: true,
ColGrandTotals: true,
ShowDrill: true,
ShowRowHeaders: true,
ShowColHeaders: true,
ShowLastColumn: true,
ShowError: true,
PivotTableStyleName: "PivotStyleLight16",
}
assert.NoError(t, f.AddPivotTable(&opts))
assert.NoError(t, f.DeletePivotTable("Sheet1", "PivotTable1"))
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddPivotTable2.xlsx")))
assert.NoError(t, f.Close())
assert.NoError(t, f.AddPivotTable(&opts))
// Test delete pivot table with unsupported table relationships charset
f.Pkg.Store("xl/tables/table1.xml", MacintoshCyrillicCharset)
assert.EqualError(t, f.DeletePivotTable("Sheet1", "PivotTable1"), "XML syntax error on line 1: invalid UTF-8")
// Test delete pivot table with unsupported worksheet relationships charset
f.Relationships.Delete("xl/worksheets/_rels/sheet1.xml.rels")
f.Pkg.Store("xl/worksheets/_rels/sheet1.xml.rels", MacintoshCyrillicCharset)
assert.EqualError(t, f.DeletePivotTable("Sheet1", "PivotTable1"), "XML syntax error on line 1: invalid UTF-8")
// Test delete pivot table without worksheet relationships
f.Relationships.Delete("xl/worksheets/_rels/sheet1.xml.rels")
f.Pkg.Delete("xl/worksheets/_rels/sheet1.xml.rels")
assert.EqualError(t, f.DeletePivotTable("Sheet1", "PivotTable1"), "table PivotTable1 does not exist")
t.Run("data_range_with_empty_column", func(t *testing.T) {
// Test add pivot table with data range doesn't organized as a list with labeled columns
f := NewFile()
// Create some data in a sheet
month := []string{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}
types := []string{"Meat", "Dairy", "Beverages", "Produce"}
assert.NoError(t, f.SetSheetRow("Sheet1", "A1", &[]string{"Month", "", "Type"}))
for row := 2; row < 32; row++ {
assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("A%d", row), month[rand.Intn(12)]))
assert.NoError(t, f.SetCellValue("Sheet1", fmt.Sprintf("C%d", row), types[rand.Intn(4)]))
}
assert.Equal(t, newPivotTableDataRangeError("parameter is invalid"), f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!A1:E31",
PivotTableRange: "Sheet1!G2:M34",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}},
Data: []PivotTableField{{Data: "Type"}},
}))
})
}
func TestParseFormatPivotTableSet(t *testing.T) {
f := NewFile()
// Create table in a worksheet
assert.NoError(t, f.AddTable("Sheet1", &Table{
Name: "Table1",
Range: "A1:D5",
}))
// Test parse format pivot table options with unsupported table relationships charset
f.Pkg.Store("xl/tables/table1.xml", MacintoshCyrillicCharset)
_, _, err := f.parseFormatPivotTableSet(&PivotTableOptions{
DataRange: "Table1",
PivotTableRange: "Sheet1!G2:K7",
Rows: []PivotTableField{{Data: "Column1"}},
})
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
} }
func TestAddPivotRowFields(t *testing.T) { func TestAddPivotRowFields(t *testing.T) {
f := NewFile() f := NewFile()
// Test invalid data range // Test invalid data range
assert.EqualError(t, f.addPivotRowFields(&xlsxPivotTableDefinition{}, &PivotTableOptions{ assert.EqualError(t, f.addPivotRowFields(&xlsxPivotTableDefinition{}, &PivotTableOptions{
DataRange: "Sheet1!$A$1:$A$1", DataRange: "Sheet1!A1:A1",
}), `parameter 'DataRange' parsing error: parameter is invalid`) }), `parameter 'DataRange' parsing error: parameter is invalid`)
} }
@ -289,7 +456,7 @@ func TestAddPivotPageFields(t *testing.T) {
f := NewFile() f := NewFile()
// Test invalid data range // Test invalid data range
assert.EqualError(t, f.addPivotPageFields(&xlsxPivotTableDefinition{}, &PivotTableOptions{ assert.EqualError(t, f.addPivotPageFields(&xlsxPivotTableDefinition{}, &PivotTableOptions{
DataRange: "Sheet1!$A$1:$A$1", DataRange: "Sheet1!A1:A1",
}), `parameter 'DataRange' parsing error: parameter is invalid`) }), `parameter 'DataRange' parsing error: parameter is invalid`)
} }
@ -297,7 +464,7 @@ func TestAddPivotDataFields(t *testing.T) {
f := NewFile() f := NewFile()
// Test invalid data range // Test invalid data range
assert.EqualError(t, f.addPivotDataFields(&xlsxPivotTableDefinition{}, &PivotTableOptions{ assert.EqualError(t, f.addPivotDataFields(&xlsxPivotTableDefinition{}, &PivotTableOptions{
DataRange: "Sheet1!$A$1:$A$1", DataRange: "Sheet1!A1:A1",
}), `parameter 'DataRange' parsing error: parameter is invalid`) }), `parameter 'DataRange' parsing error: parameter is invalid`)
} }
@ -305,19 +472,55 @@ func TestAddPivotColFields(t *testing.T) {
f := NewFile() f := NewFile()
// Test invalid data range // Test invalid data range
assert.EqualError(t, f.addPivotColFields(&xlsxPivotTableDefinition{}, &PivotTableOptions{ assert.EqualError(t, f.addPivotColFields(&xlsxPivotTableDefinition{}, &PivotTableOptions{
DataRange: "Sheet1!$A$1:$A$1", DataRange: "Sheet1!A1:A1",
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}}, Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
}), `parameter 'DataRange' parsing error: parameter is invalid`) }), `parameter 'DataRange' parsing error: parameter is invalid`)
} }
func TestGetPivotFieldsOrder(t *testing.T) { func TestGetPivotFieldsOrder(t *testing.T) {
f := NewFile() f := NewFile()
// Test get pivot fields order with not exist worksheet // Test get table fields order with not exist worksheet
_, err := f.getPivotFieldsOrder(&PivotTableOptions{DataRange: "SheetN!$A$1:$E$31"}) _, err := f.getTableFieldsOrder(&PivotTableOptions{DataRange: "SheetN!A1:E31"})
assert.EqualError(t, err, "sheet SheetN does not exist") assert.EqualError(t, err, "sheet SheetN does not exist")
// Create table in a worksheet
assert.NoError(t, f.AddTable("Sheet1", &Table{
Name: "Table1",
Range: "A1:D5",
}))
// Test get table fields order with unsupported table relationships charset
f.Pkg.Store("xl/tables/table1.xml", MacintoshCyrillicCharset)
_, err = f.getTableFieldsOrder(&PivotTableOptions{DataRange: "Table"})
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
} }
func TestGetPivotTableFieldName(t *testing.T) { func TestGetPivotTableFieldName(t *testing.T) {
f := NewFile() f := NewFile()
f.getPivotTableFieldName("-", []PivotTableField{}) assert.Empty(t, f.getPivotTableFieldName("-", []PivotTableField{}))
}
func TestGetPivotTableFieldOptions(t *testing.T) {
f := NewFile()
_, ok := f.getPivotTableFieldOptions("-", []PivotTableField{})
assert.False(t, ok)
}
func TestGenPivotCacheDefinitionID(t *testing.T) {
f := NewFile()
// Test generate pivot table cache definition ID with unsupported charset
f.Pkg.Store("xl/pivotCache/pivotCacheDefinition1.xml", MacintoshCyrillicCharset)
assert.Equal(t, 1, f.genPivotCacheDefinitionID())
assert.NoError(t, f.Close())
}
func TestDeleteWorkbookPivotCache(t *testing.T) {
f := NewFile()
// Test delete workbook pivot table cache with unsupported workbook charset
f.WorkBook = nil
f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset)
assert.EqualError(t, f.deleteWorkbookPivotCache(PivotTableOptions{pivotCacheXML: "pivotCache/pivotCacheDefinition1.xml"}), "XML syntax error on line 1: invalid UTF-8")
// Test delete workbook pivot table cache with unsupported workbook relationships charset
f.Relationships.Delete("xl/_rels/workbook.xml.rels")
f.Pkg.Store("xl/_rels/workbook.xml.rels", MacintoshCyrillicCharset)
assert.EqualError(t, f.deleteWorkbookPivotCache(PivotTableOptions{pivotCacheXML: "pivotCache/pivotCacheDefinition1.xml"}), "XML syntax error on line 1: invalid UTF-8")
} }

214
rows.go
View File

@ -1,4 +1,4 @@
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in // this source code is governed by a BSD-style license that can be found in
// the LICENSE file. // the LICENSE file.
// //
@ -7,22 +7,35 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming // Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of // API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.16 or later. // data. This library needs Go version 1.18 or later.
package excelize package excelize
import ( import (
"bytes" "bytes"
"encoding/xml" "encoding/xml"
"fmt"
"io" "io"
"math" "math"
"os" "os"
"strconv" "strconv"
"strings"
"github.com/mohae/deepcopy" "github.com/tiendc/go-deepcopy"
) )
// duplicateHelperFunc defines functions to duplicate helper.
var duplicateHelperFunc = [3]func(*File, *xlsxWorksheet, string, int, int) error{
func(f *File, ws *xlsxWorksheet, sheet string, row, row2 int) error {
return f.duplicateConditionalFormat(ws, sheet, row, row2)
},
func(f *File, ws *xlsxWorksheet, sheet string, row, row2 int) error {
return f.duplicateDataValidations(ws, sheet, row, row2)
},
func(f *File, ws *xlsxWorksheet, sheet string, row, row2 int) error {
return f.duplicateMergeCells(ws, sheet, row, row2)
},
}
// GetRows return all the rows in a sheet by given worksheet name, returned as // GetRows return all the rows in a sheet by given worksheet name, returned as
// a two-dimensional array, where the value of the cell is converted to the // a two-dimensional array, where the value of the cell is converted to the
// string type. If the cell format can be applied to the value of the cell, // string type. If the cell format can be applied to the value of the cell,
@ -50,19 +63,22 @@ func (f *File) GetRows(sheet string, opts ...Options) ([][]string, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
results, cur, max := make([][]string, 0, 64), 0, 0 results, cur, maxVal := make([][]string, 0, 64), 0, 0
for rows.Next() { for rows.Next() {
cur++ cur++
row, err := rows.Columns(opts...) row, err := rows.Columns(opts...)
if err != nil { if err != nil {
break break
} }
results = append(results, row)
if len(row) > 0 { if len(row) > 0 {
max = cur if emptyRows := cur - maxVal - 1; emptyRows > 0 {
results = append(results, make([][]string, emptyRows)...)
}
results = append(results, row)
maxVal = cur
} }
} }
return results[:max], rows.Close() return results[:maxVal], rows.Close()
} }
// Rows defines an iterator to a sheet. // Rows defines an iterator to a sheet.
@ -79,7 +95,7 @@ type Rows struct {
curRowOpts, seekRowOpts RowOpts curRowOpts, seekRowOpts RowOpts
} }
// Next will return true if find the next row element. // Next will return true if it finds the next row element.
func (rows *Rows) Next() bool { func (rows *Rows) Next() bool {
rows.seekRow++ rows.seekRow++
if rows.curRow >= rows.seekRow { if rows.curRow >= rows.seekRow {
@ -138,7 +154,7 @@ func (rows *Rows) Columns(opts ...Options) ([]string, error) {
} }
var rowIterator rowXMLIterator var rowIterator rowXMLIterator
var token xml.Token var token xml.Token
rows.rawCellValue = getOptions(opts...).RawCellValue rows.rawCellValue = rows.f.getOptions(opts...).RawCellValue
if rows.sst, rowIterator.err = rows.f.sharedStringsReader(); rowIterator.err != nil { if rows.sst, rowIterator.err = rows.f.sharedStringsReader(); rowIterator.err != nil {
return rowIterator.cells, rowIterator.err return rowIterator.cells, rowIterator.err
} }
@ -202,15 +218,6 @@ func appendSpace(l int, s []string) []string {
return s return s
} }
// ErrSheetNotExist defines an error of sheet that does not exist
type ErrSheetNotExist struct {
SheetName string
}
func (err ErrSheetNotExist) Error() string {
return fmt.Sprintf("sheet %s does not exist", err.SheetName)
}
// rowXMLIterator defined runtime use field for the worksheet row SAX parser. // rowXMLIterator defined runtime use field for the worksheet row SAX parser.
type rowXMLIterator struct { type rowXMLIterator struct {
err error err error
@ -267,12 +274,12 @@ func (f *File) Rows(sheet string) (*Rows, error) {
if !ok { if !ok {
return nil, ErrSheetNotExist{sheet} return nil, ErrSheetNotExist{sheet}
} }
if ws, ok := f.Sheet.Load(name); ok && ws != nil { if worksheet, ok := f.Sheet.Load(name); ok && worksheet != nil {
worksheet := ws.(*xlsxWorksheet) ws := worksheet.(*xlsxWorksheet)
worksheet.Lock() ws.mu.Lock()
defer worksheet.Unlock() defer ws.mu.Unlock()
// Flush data // Flush data
output, _ := xml.Marshal(worksheet) output, _ := xml.Marshal(ws)
f.saveFileList(name, f.replaceNameSpaceBytes(name, output)) f.saveFileList(name, f.replaceNameSpaceBytes(name, output))
} }
var err error var err error
@ -297,7 +304,9 @@ func (f *File) getFromStringItem(index int) string {
} }
needClose, decoder, tempFile, err := f.xmlDecoder(defaultXMLPathSharedStrings) needClose, decoder, tempFile, err := f.xmlDecoder(defaultXMLPathSharedStrings)
if needClose && err == nil { if needClose && err == nil {
defer tempFile.Close() defer func() {
err = tempFile.Close()
}()
} }
f.sharedStringItem = [][]uint{} f.sharedStringItem = [][]uint{}
f.sharedStringTemp, _ = os.CreateTemp(os.TempDir(), "excelize-") f.sharedStringTemp, _ = os.CreateTemp(os.TempDir(), "excelize-")
@ -344,8 +353,10 @@ func (f *File) xmlDecoder(name string) (bool, *xml.Decoder, *os.File, error) {
return true, f.xmlNewDecoder(tempFile), tempFile, err return true, f.xmlNewDecoder(tempFile), tempFile, err
} }
// SetRowHeight provides a function to set the height of a single row. For // SetRowHeight provides a function to set the height of a single row. If the
// example, set the height of the first row in Sheet1: // value of height is 0, will hide the specified row, if the value of height is
// -1, will unset the custom row height. For example, set the height of the
// first row in Sheet1:
// //
// err := f.SetRowHeight("Sheet1", 1, 50) // err := f.SetRowHeight("Sheet1", 1, 50)
func (f *File) SetRowHeight(sheet string, row int, height float64) error { func (f *File) SetRowHeight(sheet string, row int, height float64) error {
@ -355,31 +366,42 @@ func (f *File) SetRowHeight(sheet string, row int, height float64) error {
if height > MaxRowHeight { if height > MaxRowHeight {
return ErrMaxRowHeight return ErrMaxRowHeight
} }
if height < -1 {
return ErrParameterInvalid
}
ws, err := f.workSheetReader(sheet) ws, err := f.workSheetReader(sheet)
if err != nil { if err != nil {
return err return err
} }
prepareSheetXML(ws, 0, row) ws.prepareSheetXML(0, row)
rowIdx := row - 1 rowIdx := row - 1
if height == -1 {
ws.SheetData.Row[rowIdx].Ht = nil
ws.SheetData.Row[rowIdx].CustomHeight = false
return err
}
ws.SheetData.Row[rowIdx].Ht = float64Ptr(height) ws.SheetData.Row[rowIdx].Ht = float64Ptr(height)
ws.SheetData.Row[rowIdx].CustomHeight = true ws.SheetData.Row[rowIdx].CustomHeight = true
return nil return err
} }
// getRowHeight provides a function to get row height in pixels by given sheet // getRowHeight provides a function to get row height in pixels by given sheet
// name and row number. // name and row number.
func (f *File) getRowHeight(sheet string, row int) int { func (f *File) getRowHeight(sheet string, row int) int {
ws, _ := f.workSheetReader(sheet) ws, _ := f.workSheetReader(sheet)
ws.Lock() ws.mu.Lock()
defer ws.Unlock() defer ws.mu.Unlock()
for i := range ws.SheetData.Row { for i := range ws.SheetData.Row {
v := &ws.SheetData.Row[i] v := &ws.SheetData.Row[i]
if v.R == row && v.Ht != nil { if v.R == row && v.Ht != nil {
return int(convertRowHeightToPixels(*v.Ht)) return int(convertRowHeightToPixels(*v.Ht))
} }
} }
if ws.SheetFormatPr != nil && ws.SheetFormatPr.DefaultRowHeight > 0 {
return int(convertRowHeightToPixels(ws.SheetFormatPr.DefaultRowHeight))
}
// Optimization for when the row heights haven't changed. // Optimization for when the row heights haven't changed.
return int(defaultRowHeightPixels) return int(defaultRowHeightPixels)
} }
@ -390,7 +412,7 @@ func (f *File) getRowHeight(sheet string, row int) int {
// height, err := f.GetRowHeight("Sheet1", 1) // height, err := f.GetRowHeight("Sheet1", 1)
func (f *File) GetRowHeight(sheet string, row int) (float64, error) { func (f *File) GetRowHeight(sheet string, row int) (float64, error) {
if row < 1 { if row < 1 {
return defaultRowHeightPixels, newInvalidRowNumberError(row) return defaultRowHeight, newInvalidRowNumberError(row)
} }
ht := defaultRowHeight ht := defaultRowHeight
ws, err := f.workSheetReader(sheet) ws, err := f.workSheetReader(sheet)
@ -416,8 +438,8 @@ func (f *File) GetRowHeight(sheet string, row int) (float64, error) {
// after deserialization of xl/sharedStrings.xml. // after deserialization of xl/sharedStrings.xml.
func (f *File) sharedStringsReader() (*xlsxSST, error) { func (f *File) sharedStringsReader() (*xlsxSST, error) {
var err error var err error
f.Lock() f.mu.Lock()
defer f.Unlock() defer f.mu.Unlock()
relPath := f.getWorkbookRelsPath() relPath := f.getWorkbookRelsPath()
if f.SharedStrings == nil { if f.SharedStrings == nil {
var sharedStrings xlsxSST var sharedStrings xlsxSST
@ -470,7 +492,7 @@ func (f *File) SetRowVisible(sheet string, row int, visible bool) error {
if err != nil { if err != nil {
return err return err
} }
prepareSheetXML(ws, 0, row) ws.prepareSheetXML(0, row)
ws.SheetData.Row[row-1].Hidden = !visible ws.SheetData.Row[row-1].Hidden = !visible
return nil return nil
} }
@ -511,7 +533,7 @@ func (f *File) SetRowOutlineLevel(sheet string, row int, level uint8) error {
if err != nil { if err != nil {
return err return err
} }
prepareSheetXML(ws, 0, row) ws.prepareSheetXML(0, row)
ws.SheetData.Row[row-1].OutlineLevel = level ws.SheetData.Row[row-1].OutlineLevel = level
return nil return nil
} }
@ -623,7 +645,7 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) error {
} }
if row2 < 1 || row == row2 { if row2 < 1 || row == row2 {
return nil return err
} }
var ok bool var ok bool
@ -631,7 +653,7 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) error {
for i, r := range ws.SheetData.Row { for i, r := range ws.SheetData.Row {
if r.R == row { if r.R == row {
rowCopy = deepcopy.Copy(ws.SheetData.Row[i]).(xlsxRow) deepcopy.Copy(&rowCopy, ws.SheetData.Row[i])
ok = true ok = true
break break
} }
@ -642,7 +664,7 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) error {
} }
if !ok { if !ok {
return nil return err
} }
idx2 := -1 idx2 := -1
@ -652,24 +674,106 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) error {
break break
} }
} }
if idx2 == -1 && len(ws.SheetData.Row) >= row2 {
return nil
}
rowCopy.C = append(make([]xlsxC, 0, len(rowCopy.C)), rowCopy.C...) rowCopy.C = append(make([]xlsxC, 0, len(rowCopy.C)), rowCopy.C...)
f.adjustSingleRowDimensions(&rowCopy, row2) rowCopy.adjustSingleRowDimensions(row2 - row)
_ = f.adjustSingleRowFormulas(sheet, sheet, &rowCopy, row, row2-row, true)
if idx2 != -1 { if idx2 != -1 {
ws.SheetData.Row[idx2] = rowCopy ws.SheetData.Row[idx2] = rowCopy
} else { } else {
ws.SheetData.Row = append(ws.SheetData.Row, rowCopy) ws.SheetData.Row = append(ws.SheetData.Row, rowCopy)
} }
return f.duplicateMergeCells(sheet, ws, row, row2) for _, fn := range duplicateHelperFunc {
if err := fn(f, ws, sheet, row, row2); err != nil {
return err
}
}
return err
}
// duplicateSQRefHelper provides a function to adjust conditional formatting and
// data validations cell reference when duplicate rows.
func duplicateSQRefHelper(row, row2 int, ref string) (string, error) {
if !strings.Contains(ref, ":") {
ref += ":" + ref
}
abs := strings.Contains(ref, "$")
coordinates, err := rangeRefToCoordinates(ref)
if err != nil {
return "", err
}
x1, y1, x2, y2 := coordinates[0], coordinates[1], coordinates[2], coordinates[3]
if y1 == y2 && y1 == row {
if ref, err = coordinatesToRangeRef([]int{x1, row2, x2, row2}, abs); err != nil {
return "", err
}
return ref, err
}
return "", err
}
// duplicateConditionalFormat create conditional formatting for the destination
// row if there are conditional formats in the copied row.
func (f *File) duplicateConditionalFormat(ws *xlsxWorksheet, sheet string, row, row2 int) error {
var cfs []*xlsxConditionalFormatting
for _, cf := range ws.ConditionalFormatting {
if cf != nil {
var SQRef []string
for _, ref := range strings.Split(cf.SQRef, " ") {
coordinates, err := duplicateSQRefHelper(row, row2, ref)
if err != nil {
return err
}
if coordinates != "" {
SQRef = append(SQRef, coordinates)
}
}
if len(SQRef) > 0 {
var cfCopy xlsxConditionalFormatting
deepcopy.Copy(&cfCopy, *cf)
cfCopy.SQRef = strings.Join(SQRef, " ")
cfs = append(cfs, &cfCopy)
}
}
}
ws.ConditionalFormatting = append(ws.ConditionalFormatting, cfs...)
return nil
}
// duplicateDataValidations create data validations for the destination row if
// there are data validation rules in the copied row.
func (f *File) duplicateDataValidations(ws *xlsxWorksheet, sheet string, row, row2 int) error {
if ws.DataValidations == nil {
return nil
}
var dvs []*xlsxDataValidation
for _, dv := range ws.DataValidations.DataValidation {
if dv != nil {
var SQRef []string
for _, ref := range strings.Split(dv.Sqref, " ") {
coordinates, err := duplicateSQRefHelper(row, row2, ref)
if err != nil {
return err
}
if coordinates != "" {
SQRef = append(SQRef, coordinates)
}
}
if len(SQRef) > 0 {
var dvCopy xlsxDataValidation
deepcopy.Copy(&dvCopy, *dv)
dvCopy.Sqref = strings.Join(SQRef, " ")
dvs = append(dvs, &dvCopy)
}
}
}
ws.DataValidations.DataValidation = append(ws.DataValidations.DataValidation, dvs...)
return nil
} }
// duplicateMergeCells merge cells in the destination row if there are single // duplicateMergeCells merge cells in the destination row if there are single
// row merged cells in the copied row. // row merged cells in the copied row.
func (f *File) duplicateMergeCells(sheet string, ws *xlsxWorksheet, row, row2 int) error { func (f *File) duplicateMergeCells(ws *xlsxWorksheet, sheet string, row, row2 int) error {
if ws.MergeCells == nil { if ws.MergeCells == nil {
return nil return nil
} }
@ -703,7 +807,7 @@ func (f *File) duplicateMergeCells(sheet string, ws *xlsxWorksheet, row, row2 in
// checkRow provides a function to check and fill each column element for all // checkRow provides a function to check and fill each column element for all
// rows and make that is continuous in a worksheet of XML. For example: // rows and make that is continuous in a worksheet of XML. For example:
// //
// <row r="15" spans="1:22" x14ac:dyDescent="0.2"> // <row r="15">
// <c r="A15" s="2" /> // <c r="A15" s="2" />
// <c r="B15" s="2" /> // <c r="B15" s="2" />
// <c r="F15" s="1" /> // <c r="F15" s="1" />
@ -712,7 +816,7 @@ func (f *File) duplicateMergeCells(sheet string, ws *xlsxWorksheet, row, row2 in
// //
// in this case, we should to change it to // in this case, we should to change it to
// //
// <row r="15" spans="1:22" x14ac:dyDescent="0.2"> // <row r="15">
// <c r="A15" s="2" /> // <c r="A15" s="2" />
// <c r="B15" s="2" /> // <c r="B15" s="2" />
// <c r="C15" s="2" /> // <c r="C15" s="2" />
@ -724,7 +828,7 @@ func (f *File) duplicateMergeCells(sheet string, ws *xlsxWorksheet, row, row2 in
// //
// Notice: this method could be very slow for large spreadsheets (more than // Notice: this method could be very slow for large spreadsheets (more than
// 3000 rows one sheet). // 3000 rows one sheet).
func checkRow(ws *xlsxWorksheet) error { func (ws *xlsxWorksheet) checkRow() error {
for rowIdx := range ws.SheetData.Row { for rowIdx := range ws.SheetData.Row {
rowData := &ws.SheetData.Row[rowIdx] rowData := &ws.SheetData.Row[rowIdx]
@ -814,8 +918,8 @@ func (f *File) SetRowStyle(sheet string, start, end, styleID int) error {
if err != nil { if err != nil {
return err return err
} }
s.Lock() s.mu.Lock()
defer s.Unlock() defer s.mu.Unlock()
if styleID < 0 || s.CellXfs == nil || len(s.CellXfs.Xf) <= styleID { if styleID < 0 || s.CellXfs == nil || len(s.CellXfs.Xf) <= styleID {
return newInvalidStyleID(styleID) return newInvalidStyleID(styleID)
} }
@ -823,7 +927,7 @@ func (f *File) SetRowStyle(sheet string, start, end, styleID int) error {
if err != nil { if err != nil {
return err return err
} }
prepareSheetXML(ws, 0, end) ws.prepareSheetXML(0, end)
for row := start - 1; row < end; row++ { for row := start - 1; row < end; row++ {
ws.SheetData.Row[row].S = styleID ws.SheetData.Row[row].S = styleID
ws.SheetData.Row[row].CustomFormat = true ws.SheetData.Row[row].CustomFormat = true
@ -840,10 +944,8 @@ func (f *File) SetRowStyle(sheet string, start, end, styleID int) error {
// cell from user's units to pixels. If the height hasn't been set by the user // cell from user's units to pixels. If the height hasn't been set by the user
// we use the default value. If the row is hidden it has a value of zero. // we use the default value. If the row is hidden it has a value of zero.
func convertRowHeightToPixels(height float64) float64 { func convertRowHeightToPixels(height float64) float64 {
var pixels float64
if height == 0 { if height == 0 {
return pixels return 0
} }
pixels = math.Ceil(4.0 / 3.0 * height) return math.Ceil(4.0 / 3.4 * height)
return pixels
} }

View File

@ -11,6 +11,16 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestGetRows(t *testing.T) {
f := NewFile()
assert.NoError(t, f.SetCellValue("Sheet1", "A1", "A1"))
// Test get rows with unsupported charset shared strings table
f.SharedStrings = nil
f.Pkg.Store(defaultXMLPathSharedStrings, MacintoshCyrillicCharset)
_, err := f.GetRows("Sheet1")
assert.NoError(t, err)
}
func TestRows(t *testing.T) { func TestRows(t *testing.T) {
const sheet2 = "Sheet2" const sheet2 = "Sheet2"
f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
@ -229,7 +239,7 @@ func TestColumns(t *testing.T) {
rows.decoder = f.xmlNewDecoder(bytes.NewReader([]byte(`<worksheet><sheetData><row r="1"><c r="A" t="s"><v>1</v></c></row></sheetData></worksheet>`))) rows.decoder = f.xmlNewDecoder(bytes.NewReader([]byte(`<worksheet><sheetData><row r="1"><c r="A" t="s"><v>1</v></c></row></sheetData></worksheet>`)))
assert.True(t, rows.Next()) assert.True(t, rows.Next())
_, err = rows.Columns() _, err = rows.Columns()
assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), err)
// Test token is nil // Test token is nil
rows.decoder = f.xmlNewDecoder(bytes.NewReader(nil)) rows.decoder = f.xmlNewDecoder(bytes.NewReader(nil))
@ -295,7 +305,7 @@ func TestRemoveRow(t *testing.T) {
colCount = 10 colCount = 10
rowCount = 10 rowCount = 10
) )
fillCells(f, sheet1, colCount, rowCount) assert.NoError(t, fillCells(f, sheet1, colCount, rowCount))
assert.NoError(t, f.SetCellHyperLink(sheet1, "A5", "https://github.com/xuri/excelize", "External")) assert.NoError(t, f.SetCellHyperLink(sheet1, "A5", "https://github.com/xuri/excelize", "External"))
@ -343,6 +353,15 @@ func TestRemoveRow(t *testing.T) {
assert.NoError(t, f.RemoveRow(sheet1, 10)) assert.NoError(t, f.RemoveRow(sheet1, 10))
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestRemoveRow.xlsx"))) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestRemoveRow.xlsx")))
f = NewFile()
assert.NoError(t, f.MergeCell("Sheet1", "A1", "C1"))
assert.NoError(t, f.MergeCell("Sheet1", "A2", "C2"))
assert.NoError(t, f.RemoveRow("Sheet1", 1))
mergedCells, err := f.GetMergeCells("Sheet1")
assert.NoError(t, err)
assert.Equal(t, "A1", mergedCells[0].GetStartAxis())
assert.Equal(t, "C1", mergedCells[0].GetEndAxis())
// Test remove row on not exist worksheet // Test remove row on not exist worksheet
assert.EqualError(t, f.RemoveRow("SheetN", 1), "sheet SheetN does not exist") assert.EqualError(t, f.RemoveRow("SheetN", 1), "sheet SheetN does not exist")
// Test remove row with invalid sheet name // Test remove row with invalid sheet name
@ -358,7 +377,7 @@ func TestInsertRows(t *testing.T) {
colCount = 10 colCount = 10
rowCount = 10 rowCount = 10
) )
fillCells(f, sheet1, colCount, rowCount) assert.NoError(t, fillCells(f, sheet1, colCount, rowCount))
assert.NoError(t, f.SetCellHyperLink(sheet1, "A5", "https://github.com/xuri/excelize", "External")) assert.NoError(t, f.SetCellHyperLink(sheet1, "A5", "https://github.com/xuri/excelize", "External"))
@ -406,6 +425,23 @@ func TestInsertRowsInEmptyFile(t *testing.T) {
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestInsertRowInEmptyFile.xlsx"))) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestInsertRowInEmptyFile.xlsx")))
} }
func prepareTestBook2() (*File, error) {
f := NewFile()
for cell, val := range map[string]string{
"A1": "A1 Value",
"A2": "A2 Value",
"A3": "A3 Value",
"B1": "B1 Value",
"B2": "B2 Value",
"B3": "B3 Value",
} {
if err := f.SetCellStr("Sheet1", cell, val); err != nil {
return f, err
}
}
return f, nil
}
func TestDuplicateRowFromSingleRow(t *testing.T) { func TestDuplicateRowFromSingleRow(t *testing.T) {
const sheet = "Sheet1" const sheet = "Sheet1"
outFile := filepath.Join("test", "TestDuplicateRow.%s.xlsx") outFile := filepath.Join("test", "TestDuplicateRow.%s.xlsx")
@ -502,7 +538,6 @@ func TestDuplicateRowUpdateDuplicatedRows(t *testing.T) {
func TestDuplicateRowFirstOfMultipleRows(t *testing.T) { func TestDuplicateRowFirstOfMultipleRows(t *testing.T) {
const sheet = "Sheet1" const sheet = "Sheet1"
outFile := filepath.Join("test", "TestDuplicateRow.%s.xlsx") outFile := filepath.Join("test", "TestDuplicateRow.%s.xlsx")
cells := map[string]string{ cells := map[string]string{
"A1": "A1 Value", "A1": "A1 Value",
"A2": "A2 Value", "A2": "A2 Value",
@ -511,18 +546,9 @@ func TestDuplicateRowFirstOfMultipleRows(t *testing.T) {
"B2": "B2 Value", "B2": "B2 Value",
"B3": "B3 Value", "B3": "B3 Value",
} }
newFileWithDefaults := func() *File {
f := NewFile()
for cell, val := range cells {
assert.NoError(t, f.SetCellStr(sheet, cell, val))
}
return f
}
t.Run("FirstOfMultipleRows", func(t *testing.T) { t.Run("FirstOfMultipleRows", func(t *testing.T) {
f := newFileWithDefaults() f, err := prepareTestBook2()
assert.NoError(t, err)
assert.NoError(t, f.DuplicateRow(sheet, 1)) assert.NoError(t, f.DuplicateRow(sheet, 1))
if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "FirstOfMultipleRows"))) { if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "FirstOfMultipleRows"))) {
@ -625,18 +651,9 @@ func TestDuplicateRowWithLargeOffsetToMiddleOfData(t *testing.T) {
"B2": "B2 Value", "B2": "B2 Value",
"B3": "B3 Value", "B3": "B3 Value",
} }
newFileWithDefaults := func() *File {
f := NewFile()
for cell, val := range cells {
assert.NoError(t, f.SetCellStr(sheet, cell, val))
}
return f
}
t.Run("WithLargeOffsetToMiddleOfData", func(t *testing.T) { t.Run("WithLargeOffsetToMiddleOfData", func(t *testing.T) {
f := newFileWithDefaults() f, err := prepareTestBook2()
assert.NoError(t, err)
assert.NoError(t, f.DuplicateRowTo(sheet, 1, 3)) assert.NoError(t, f.DuplicateRowTo(sheet, 1, 3))
if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "WithLargeOffsetToMiddleOfData"))) { if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "WithLargeOffsetToMiddleOfData"))) {
@ -661,7 +678,6 @@ func TestDuplicateRowWithLargeOffsetToMiddleOfData(t *testing.T) {
func TestDuplicateRowWithLargeOffsetToEmptyRows(t *testing.T) { func TestDuplicateRowWithLargeOffsetToEmptyRows(t *testing.T) {
const sheet = "Sheet1" const sheet = "Sheet1"
outFile := filepath.Join("test", "TestDuplicateRow.%s.xlsx") outFile := filepath.Join("test", "TestDuplicateRow.%s.xlsx")
cells := map[string]string{ cells := map[string]string{
"A1": "A1 Value", "A1": "A1 Value",
"A2": "A2 Value", "A2": "A2 Value",
@ -670,18 +686,9 @@ func TestDuplicateRowWithLargeOffsetToEmptyRows(t *testing.T) {
"B2": "B2 Value", "B2": "B2 Value",
"B3": "B3 Value", "B3": "B3 Value",
} }
newFileWithDefaults := func() *File {
f := NewFile()
for cell, val := range cells {
assert.NoError(t, f.SetCellStr(sheet, cell, val))
}
return f
}
t.Run("WithLargeOffsetToEmptyRows", func(t *testing.T) { t.Run("WithLargeOffsetToEmptyRows", func(t *testing.T) {
f := newFileWithDefaults() f, err := prepareTestBook2()
assert.NoError(t, err)
assert.NoError(t, f.DuplicateRowTo(sheet, 1, 7)) assert.NoError(t, f.DuplicateRowTo(sheet, 1, 7))
if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "WithLargeOffsetToEmptyRows"))) { if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "WithLargeOffsetToEmptyRows"))) {
@ -706,7 +713,6 @@ func TestDuplicateRowWithLargeOffsetToEmptyRows(t *testing.T) {
func TestDuplicateRowInsertBefore(t *testing.T) { func TestDuplicateRowInsertBefore(t *testing.T) {
const sheet = "Sheet1" const sheet = "Sheet1"
outFile := filepath.Join("test", "TestDuplicateRow.%s.xlsx") outFile := filepath.Join("test", "TestDuplicateRow.%s.xlsx")
cells := map[string]string{ cells := map[string]string{
"A1": "A1 Value", "A1": "A1 Value",
"A2": "A2 Value", "A2": "A2 Value",
@ -715,18 +721,9 @@ func TestDuplicateRowInsertBefore(t *testing.T) {
"B2": "B2 Value", "B2": "B2 Value",
"B3": "B3 Value", "B3": "B3 Value",
} }
newFileWithDefaults := func() *File {
f := NewFile()
for cell, val := range cells {
assert.NoError(t, f.SetCellStr(sheet, cell, val))
}
return f
}
t.Run("InsertBefore", func(t *testing.T) { t.Run("InsertBefore", func(t *testing.T) {
f := newFileWithDefaults() f, err := prepareTestBook2()
assert.NoError(t, err)
assert.NoError(t, f.DuplicateRowTo(sheet, 2, 1)) assert.NoError(t, f.DuplicateRowTo(sheet, 2, 1))
assert.NoError(t, f.DuplicateRowTo(sheet, 10, 4)) assert.NoError(t, f.DuplicateRowTo(sheet, 10, 4))
@ -753,7 +750,6 @@ func TestDuplicateRowInsertBefore(t *testing.T) {
func TestDuplicateRowInsertBeforeWithLargeOffset(t *testing.T) { func TestDuplicateRowInsertBeforeWithLargeOffset(t *testing.T) {
const sheet = "Sheet1" const sheet = "Sheet1"
outFile := filepath.Join("test", "TestDuplicateRow.%s.xlsx") outFile := filepath.Join("test", "TestDuplicateRow.%s.xlsx")
cells := map[string]string{ cells := map[string]string{
"A1": "A1 Value", "A1": "A1 Value",
"A2": "A2 Value", "A2": "A2 Value",
@ -762,18 +758,9 @@ func TestDuplicateRowInsertBeforeWithLargeOffset(t *testing.T) {
"B2": "B2 Value", "B2": "B2 Value",
"B3": "B3 Value", "B3": "B3 Value",
} }
newFileWithDefaults := func() *File {
f := NewFile()
for cell, val := range cells {
assert.NoError(t, f.SetCellStr(sheet, cell, val))
}
return f
}
t.Run("InsertBeforeWithLargeOffset", func(t *testing.T) { t.Run("InsertBeforeWithLargeOffset", func(t *testing.T) {
f := newFileWithDefaults() f, err := prepareTestBook2()
assert.NoError(t, err)
assert.NoError(t, f.DuplicateRowTo(sheet, 3, 1)) assert.NoError(t, f.DuplicateRowTo(sheet, 3, 1))
if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "InsertBeforeWithLargeOffset"))) { if !assert.NoError(t, f.SaveAs(fmt.Sprintf(outFile, "InsertBeforeWithLargeOffset"))) {
@ -799,28 +786,11 @@ func TestDuplicateRowInsertBeforeWithLargeOffset(t *testing.T) {
func TestDuplicateRowInsertBeforeWithMergeCells(t *testing.T) { func TestDuplicateRowInsertBeforeWithMergeCells(t *testing.T) {
const sheet = "Sheet1" const sheet = "Sheet1"
outFile := filepath.Join("test", "TestDuplicateRow.%s.xlsx") outFile := filepath.Join("test", "TestDuplicateRow.%s.xlsx")
t.Run("InsertBeforeWithLargeOffset", func(t *testing.T) {
cells := map[string]string{ f, err := prepareTestBook2()
"A1": "A1 Value", assert.NoError(t, err)
"A2": "A2 Value",
"A3": "A3 Value",
"B1": "B1 Value",
"B2": "B2 Value",
"B3": "B3 Value",
}
newFileWithDefaults := func() *File {
f := NewFile()
for cell, val := range cells {
assert.NoError(t, f.SetCellStr(sheet, cell, val))
}
assert.NoError(t, f.MergeCell(sheet, "B2", "C2")) assert.NoError(t, f.MergeCell(sheet, "B2", "C2"))
assert.NoError(t, f.MergeCell(sheet, "C6", "C8")) assert.NoError(t, f.MergeCell(sheet, "C6", "C8"))
return f
}
t.Run("InsertBeforeWithLargeOffset", func(t *testing.T) {
f := newFileWithDefaults()
assert.NoError(t, f.DuplicateRowTo(sheet, 2, 1)) assert.NoError(t, f.DuplicateRowTo(sheet, 2, 1))
assert.NoError(t, f.DuplicateRowTo(sheet, 1, 8)) assert.NoError(t, f.DuplicateRowTo(sheet, 1, 8))
@ -909,6 +879,60 @@ func TestDuplicateRow(t *testing.T) {
f := NewFile() f := NewFile()
// Test duplicate row with invalid sheet name // Test duplicate row with invalid sheet name
assert.EqualError(t, f.DuplicateRowTo("Sheet:1", 1, 2), ErrSheetNameInvalid.Error()) assert.EqualError(t, f.DuplicateRowTo("Sheet:1", 1, 2), ErrSheetNameInvalid.Error())
f = NewFile()
assert.NoError(t, f.SetDefinedName(&DefinedName{
Name: "Amount",
RefersTo: "Sheet1!$B$1",
}))
assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "Amount+C1"))
assert.NoError(t, f.SetCellValue("Sheet1", "A10", "A10"))
format, err := f.NewConditionalStyle(&Style{Font: &Font{Color: "9A0511"}, Fill: Fill{Type: "pattern", Color: []string{"FEC7CE"}, Pattern: 1}})
assert.NoError(t, err)
expected := []ConditionalFormatOptions{
{Type: "cell", Criteria: "greater than", Format: &format, Value: "0"},
}
assert.NoError(t, f.SetConditionalFormat("Sheet1", "A1", expected))
dv := NewDataValidation(true)
dv.Sqref = "A1"
assert.NoError(t, dv.SetDropList([]string{"1", "2", "3"}))
assert.NoError(t, f.AddDataValidation("Sheet1", dv))
ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
assert.True(t, ok)
ws.(*xlsxWorksheet).DataValidations.DataValidation[0].Sqref = "A1"
assert.NoError(t, f.DuplicateRowTo("Sheet1", 1, 10))
formula, err := f.GetCellFormula("Sheet1", "A10")
assert.NoError(t, err)
assert.Equal(t, "Amount+C10", formula)
value, err := f.GetCellValue("Sheet1", "A11")
assert.NoError(t, err)
assert.Equal(t, "A10", value)
cfs, err := f.GetConditionalFormats("Sheet1")
assert.NoError(t, err)
assert.Len(t, cfs, 2)
assert.Equal(t, expected, cfs["A10:A10"])
dvs, err := f.GetDataValidations("Sheet1")
assert.NoError(t, err)
assert.Len(t, dvs, 2)
assert.Equal(t, "A10:A10", dvs[1].Sqref)
// Test duplicate data validation with row number exceeds maximum limit
assert.Equal(t, ErrMaxRows, f.duplicateDataValidations(ws.(*xlsxWorksheet), "Sheet1", 1, TotalRows+1))
// Test duplicate data validation with invalid range reference
ws.(*xlsxWorksheet).DataValidations.DataValidation[0].Sqref = "A"
assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.duplicateDataValidations(ws.(*xlsxWorksheet), "Sheet1", 1, 10))
// Test duplicate conditional formatting with row number exceeds maximum limit
assert.Equal(t, ErrMaxRows, f.duplicateConditionalFormat(ws.(*xlsxWorksheet), "Sheet1", 1, TotalRows+1))
// Test duplicate conditional formatting with invalid range reference
ws.(*xlsxWorksheet).ConditionalFormatting[0].SQRef = "A"
assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.duplicateConditionalFormat(ws.(*xlsxWorksheet), "Sheet1", 1, 10))
} }
func TestDuplicateRowTo(t *testing.T) { func TestDuplicateRowTo(t *testing.T) {
@ -935,9 +959,9 @@ func TestDuplicateMergeCells(t *testing.T) {
ws := &xlsxWorksheet{MergeCells: &xlsxMergeCells{ ws := &xlsxWorksheet{MergeCells: &xlsxMergeCells{
Cells: []*xlsxMergeCell{{Ref: "A1:-"}}, Cells: []*xlsxMergeCell{{Ref: "A1:-"}},
}} }}
assert.EqualError(t, f.duplicateMergeCells("Sheet1", ws, 0, 0), `cannot convert cell "-" to coordinates: invalid cell name "-"`) assert.EqualError(t, f.duplicateMergeCells(ws, "Sheet1", 0, 0), `cannot convert cell "-" to coordinates: invalid cell name "-"`)
ws.MergeCells.Cells[0].Ref = "A1:B1" ws.MergeCells.Cells[0].Ref = "A1:B1"
assert.EqualError(t, f.duplicateMergeCells("SheetN", ws, 1, 2), "sheet SheetN does not exist") assert.EqualError(t, f.duplicateMergeCells(ws, "SheetN", 1, 2), "sheet SheetN does not exist")
} }
func TestGetValueFromInlineStr(t *testing.T) { func TestGetValueFromInlineStr(t *testing.T) {
@ -971,8 +995,7 @@ func TestGetValueFromNumber(t *testing.T) {
} }
func TestErrSheetNotExistError(t *testing.T) { func TestErrSheetNotExistError(t *testing.T) {
err := ErrSheetNotExist{SheetName: "Sheet1"} assert.Equal(t, "sheet Sheet1 does not exist", ErrSheetNotExist{"Sheet1"}.Error())
assert.EqualValues(t, err.Error(), "sheet Sheet1 does not exist")
} }
func TestCheckRow(t *testing.T) { func TestCheckRow(t *testing.T) {
@ -984,7 +1007,7 @@ func TestCheckRow(t *testing.T) {
f = NewFile() f = NewFile()
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(xml.Header+`<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" ><sheetData><row r="2"><c><v>1</v></c><c r="-"><v>2</v></c><c><v>3</v></c><c><v>4</v></c><c r="M2"><v>5</v></c></row></sheetData></worksheet>`)) f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(xml.Header+`<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" ><sheetData><row r="2"><c><v>1</v></c><c r="-"><v>2</v></c><c><v>3</v></c><c><v>4</v></c><c r="M2"><v>5</v></c></row></sheetData></worksheet>`))
f.Sheet.Delete("xl/worksheets/sheet1.xml") f.Sheet.Delete("xl/worksheets/sheet1.xml")
delete(f.checked, "xl/worksheets/sheet1.xml") f.checked.Delete("xl/worksheets/sheet1.xml")
assert.EqualError(t, f.SetCellValue("Sheet1", "A1", false), newCellNameToCoordinatesError("-", newInvalidCellNameError("-")).Error()) assert.EqualError(t, f.SetCellValue("Sheet1", "A1", false), newCellNameToCoordinatesError("-", newInvalidCellNameError("-")).Error())
} }
@ -1020,6 +1043,22 @@ func TestSetRowStyle(t *testing.T) {
assert.EqualError(t, f.SetRowStyle("Sheet1", 1, 1, cellStyleID), "XML syntax error on line 1: invalid UTF-8") assert.EqualError(t, f.SetRowStyle("Sheet1", 1, 1, cellStyleID), "XML syntax error on line 1: invalid UTF-8")
} }
func TestSetRowHeight(t *testing.T) {
f := NewFile()
// Test hidden row by set row height to 0
assert.NoError(t, f.SetRowHeight("Sheet1", 2, 0))
ht, err := f.GetRowHeight("Sheet1", 2)
assert.NoError(t, err)
assert.Empty(t, ht)
// Test unset custom row height
assert.NoError(t, f.SetRowHeight("Sheet1", 2, -1))
ht, err = f.GetRowHeight("Sheet1", 2)
assert.NoError(t, err)
assert.Equal(t, defaultRowHeight, ht)
// Test set row height with invalid height value
assert.Equal(t, ErrParameterInvalid, f.SetRowHeight("Sheet1", 2, -2))
}
func TestNumberFormats(t *testing.T) { func TestNumberFormats(t *testing.T) {
f, err := OpenFile(filepath.Join("test", "Book1.xlsx")) f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
if !assert.NoError(t, err) { if !assert.NoError(t, err) {
@ -1104,9 +1143,18 @@ func TestNumberFormats(t *testing.T) {
assert.NoError(t, f.SetCellValue("Sheet1", cell, value)) assert.NoError(t, f.SetCellValue("Sheet1", cell, value))
result, err := f.GetCellValue("Sheet1", cell) result, err := f.GetCellValue("Sheet1", cell)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, expected, result) assert.Equal(t, expected, result, cell)
} }
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestNumberFormats.xlsx"))) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestNumberFormats.xlsx")))
f = NewFile(Options{ShortDatePattern: "yyyy/m/d"})
assert.NoError(t, f.SetCellValue("Sheet1", "A1", 43543.503206018519))
numFmt14, err := f.NewStyle(&Style{NumFmt: 14})
assert.NoError(t, err)
assert.NoError(t, f.SetCellStyle("Sheet1", "A1", "A1", numFmt14))
result, err := f.GetCellValue("Sheet1", "A1")
assert.NoError(t, err)
assert.Equal(t, "2019/3/19", result, "A1")
} }
func BenchmarkRows(b *testing.B) { func BenchmarkRows(b *testing.B) {

View File

@ -1,4 +1,4 @@
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in // this source code is governed by a BSD-style license that can be found in
// the LICENSE file. // the LICENSE file.
// //
@ -7,7 +7,7 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming // Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of // API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.16 or later. // data. This library needs Go version 1.18 or later.
package excelize package excelize
@ -22,6 +22,9 @@ func parseShapeOptions(opts *Shape) (*Shape, error) {
if opts == nil { if opts == nil {
return nil, ErrParameterInvalid return nil, ErrParameterInvalid
} }
if opts.Type == "" {
return nil, ErrParameterInvalid
}
if opts.Width == 0 { if opts.Width == 0 {
opts.Width = defaultShapeSize opts.Width = defaultShapeSize
} }
@ -35,10 +38,10 @@ func parseShapeOptions(opts *Shape) (*Shape, error) {
opts.Format.Locked = boolPtr(false) opts.Format.Locked = boolPtr(false)
} }
if opts.Format.ScaleX == 0 { if opts.Format.ScaleX == 0 {
opts.Format.ScaleX = defaultPictureScale opts.Format.ScaleX = defaultDrawingScale
} }
if opts.Format.ScaleY == 0 { if opts.Format.ScaleY == 0 {
opts.Format.ScaleY = defaultPictureScale opts.Format.ScaleY = defaultDrawingScale
} }
if opts.Line.Width == nil { if opts.Line.Width == nil {
opts.Line.Width = float64Ptr(defaultShapeLineWidth) opts.Line.Width = float64Ptr(defaultShapeLineWidth)
@ -47,13 +50,13 @@ func parseShapeOptions(opts *Shape) (*Shape, error) {
} }
// AddShape provides the method to add shape in a sheet by given worksheet // AddShape provides the method to add shape in a sheet by given worksheet
// index, shape format set (such as offset, scale, aspect ratio setting and // name and shape format set (such as offset, scale, aspect ratio setting and
// print settings) and properties set. For example, add text box (rect shape) // print settings). For example, add text box (rect shape) in Sheet1:
// in Sheet1:
// //
// lineWidth := 1.2 // lineWidth := 1.2
// err := f.AddShape("Sheet1", "G6", // err := f.AddShape("Sheet1",
// &excelize.Shape{ // &excelize.Shape{
// Cell: "G6",
// Type: "rect", // Type: "rect",
// Line: excelize.ShapeLine{Color: "4286F4", Width: &lineWidth}, // Line: excelize.ShapeLine{Color: "4286F4", Width: &lineWidth},
// Fill: excelize.Fill{Color: []string{"8EB9FF"}, Pattern: 1}, // Fill: excelize.Fill{Color: []string{"8EB9FF"}, Pattern: 1},
@ -285,12 +288,12 @@ func parseShapeOptions(opts *Shape) (*Shape, error) {
// wavy // wavy
// wavyHeavy // wavyHeavy
// wavyDbl // wavyDbl
func (f *File) AddShape(sheet, cell string, opts *Shape) error { func (f *File) AddShape(sheet string, opts *Shape) error {
options, err := parseShapeOptions(opts) options, err := parseShapeOptions(opts)
if err != nil { if err != nil {
return err return err
} }
// Read sheet data. // Read sheet data
ws, err := f.workSheetReader(sheet) ws, err := f.workSheetReader(sheet)
if err != nil { if err != nil {
return err return err
@ -313,38 +316,33 @@ func (f *File) AddShape(sheet, cell string, opts *Shape) error {
f.addSheetDrawing(sheet, rID) f.addSheetDrawing(sheet, rID)
f.addSheetNameSpace(sheet, SourceRelationship) f.addSheetNameSpace(sheet, SourceRelationship)
} }
if err = f.addDrawingShape(sheet, drawingXML, cell, options); err != nil { if err = f.addDrawingShape(sheet, drawingXML, opts.Cell, options); err != nil {
return err return err
} }
return f.addContentTypePart(drawingID, "drawings") return f.addContentTypePart(drawingID, "drawings")
} }
// addDrawingShape provides a function to add preset geometry by given sheet, // twoCellAnchorShape create a two cell anchor shape size placeholder for a
// drawingXMLand format sets. // group, a shape, or a drawing element.
func (f *File) addDrawingShape(sheet, drawingXML, cell string, opts *Shape) error { func (f *File) twoCellAnchorShape(sheet, drawingXML, cell string, width, height uint, format GraphicOptions) (*xlsxWsDr, *xdrCellAnchor, int, error) {
fromCol, fromRow, err := CellNameToCoordinates(cell) fromCol, fromRow, err := CellNameToCoordinates(cell)
if err != nil { if err != nil {
return err return nil, nil, 0, err
} }
colIdx := fromCol - 1 w := int(float64(width) * format.ScaleX)
rowIdx := fromRow - 1 h := int(float64(height) * format.ScaleY)
colStart, rowStart, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(sheet, fromCol, fromRow, format.OffsetX, format.OffsetY, w, h)
width := int(float64(opts.Width) * opts.Format.ScaleX)
height := int(float64(opts.Height) * opts.Format.ScaleY)
colStart, rowStart, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(sheet, colIdx, rowIdx, opts.Format.OffsetX, opts.Format.OffsetY,
width, height)
content, cNvPrID, err := f.drawingParser(drawingXML) content, cNvPrID, err := f.drawingParser(drawingXML)
if err != nil { if err != nil {
return err return content, nil, cNvPrID, err
} }
twoCellAnchor := xdrCellAnchor{} twoCellAnchor := xdrCellAnchor{}
twoCellAnchor.EditAs = opts.Format.Positioning twoCellAnchor.EditAs = format.Positioning
from := xlsxFrom{} from := xlsxFrom{}
from.Col = colStart from.Col = colStart
from.ColOff = opts.Format.OffsetX * EMU from.ColOff = format.OffsetX * EMU
from.Row = rowStart from.Row = rowStart
from.RowOff = opts.Format.OffsetY * EMU from.RowOff = format.OffsetY * EMU
to := xlsxTo{} to := xlsxTo{}
to.Col = colEnd to.Col = colEnd
to.ColOff = x2 * EMU to.ColOff = x2 * EMU
@ -352,6 +350,17 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, opts *Shape) erro
to.RowOff = y2 * EMU to.RowOff = y2 * EMU
twoCellAnchor.From = &from twoCellAnchor.From = &from
twoCellAnchor.To = &to twoCellAnchor.To = &to
return content, &twoCellAnchor, cNvPrID, err
}
// addDrawingShape provides a function to add preset geometry by given sheet,
// drawingXML and format sets.
func (f *File) addDrawingShape(sheet, drawingXML, cell string, opts *Shape) error {
content, twoCellAnchor, cNvPrID, err := f.twoCellAnchorShape(
sheet, drawingXML, cell, opts.Width, opts.Height, opts.Format)
if err != nil {
return err
}
var solidColor string var solidColor string
if len(opts.Fill.Color) == 1 { if len(opts.Fill.Color) == 1 {
solidColor = opts.Fill.Color[0] solidColor = opts.Fill.Color[0]
@ -462,7 +471,7 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, opts *Shape) erro
FLocksWithSheet: *opts.Format.Locked, FLocksWithSheet: *opts.Format.Locked,
FPrintsWithSheet: *opts.Format.PrintObject, FPrintsWithSheet: *opts.Format.PrintObject,
} }
content.TwoCellAnchor = append(content.TwoCellAnchor, &twoCellAnchor) content.TwoCellAnchor = append(content.TwoCellAnchor, twoCellAnchor)
f.Drawings.Store(drawingXML, content) f.Drawings.Store(drawingXML, content)
return err return err
} }

View File

@ -12,18 +12,19 @@ func TestAddShape(t *testing.T) {
if !assert.NoError(t, err) { if !assert.NoError(t, err) {
t.FailNow() t.FailNow()
} }
shape := &Shape{ assert.NoError(t, f.AddShape("Sheet1", &Shape{
Cell: "A30",
Type: "rect", Type: "rect",
Paragraph: []RichTextRun{ Paragraph: []RichTextRun{
{Text: "Rectangle", Font: &Font{Color: "CD5C5C"}}, {Text: "Rectangle", Font: &Font{Color: "CD5C5C"}},
{Text: "Shape", Font: &Font{Bold: true, Color: "2980B9"}}, {Text: "Shape", Font: &Font{Bold: true, Color: "2980B9"}},
}, },
} }))
assert.NoError(t, f.AddShape("Sheet1", "A30", shape)) assert.NoError(t, f.AddShape("Sheet1", &Shape{Cell: "B30", Type: "rect", Paragraph: []RichTextRun{{Text: "Rectangle"}, {}}}))
assert.NoError(t, f.AddShape("Sheet1", "B30", &Shape{Type: "rect", Paragraph: []RichTextRun{{Text: "Rectangle"}, {}}})) assert.NoError(t, f.AddShape("Sheet1", &Shape{Cell: "C30", Type: "rect"}))
assert.NoError(t, f.AddShape("Sheet1", "C30", &Shape{Type: "rect"})) assert.EqualError(t, f.AddShape("Sheet3",
assert.EqualError(t, f.AddShape("Sheet3", "H1",
&Shape{ &Shape{
Cell: "H1",
Type: "ellipseRibbon", Type: "ellipseRibbon",
Line: ShapeLine{Color: "4286F4"}, Line: ShapeLine{Color: "4286F4"},
Fill: Fill{Color: []string{"8EB9FF"}}, Fill: Fill{Color: []string{"8EB9FF"}},
@ -41,15 +42,24 @@ func TestAddShape(t *testing.T) {
}, },
}, },
), "sheet Sheet3 does not exist") ), "sheet Sheet3 does not exist")
assert.EqualError(t, f.AddShape("Sheet3", "H1", nil), ErrParameterInvalid.Error()) assert.Equal(t, ErrParameterInvalid, f.AddShape("Sheet3", nil))
assert.EqualError(t, f.AddShape("Sheet1", "A", shape), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.Equal(t, ErrParameterInvalid, f.AddShape("Sheet1", &Shape{Cell: "A1"}))
assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.AddShape("Sheet1", &Shape{
Cell: "A",
Type: "rect",
Paragraph: []RichTextRun{
{Text: "Rectangle", Font: &Font{Color: "CD5C5C"}},
{Text: "Shape", Font: &Font{Bold: true, Color: "2980B9"}},
},
}))
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddShape1.xlsx"))) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddShape1.xlsx")))
// Test add first shape for given sheet // Test add first shape for given sheet
f = NewFile() f = NewFile()
lineWidth := 1.2 lineWidth := 1.2
assert.NoError(t, f.AddShape("Sheet1", "A1", assert.NoError(t, f.AddShape("Sheet1",
&Shape{ &Shape{
Cell: "A1",
Type: "ellipseRibbon", Type: "ellipseRibbon",
Line: ShapeLine{Color: "4286F4", Width: &lineWidth}, Line: ShapeLine{Color: "4286F4", Width: &lineWidth},
Fill: Fill{Color: []string{"8EB9FF"}}, Fill: Fill{Color: []string{"8EB9FF"}},
@ -69,16 +79,23 @@ func TestAddShape(t *testing.T) {
})) }))
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddShape2.xlsx"))) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddShape2.xlsx")))
// Test add shape with invalid sheet name // Test add shape with invalid sheet name
assert.EqualError(t, f.AddShape("Sheet:1", "A30", shape), ErrSheetNameInvalid.Error()) assert.Equal(t, ErrSheetNameInvalid, f.AddShape("Sheet:1", &Shape{
Cell: "A30",
Type: "rect",
Paragraph: []RichTextRun{
{Text: "Rectangle", Font: &Font{Color: "CD5C5C"}},
{Text: "Shape", Font: &Font{Bold: true, Color: "2980B9"}},
},
}))
// Test add shape with unsupported charset style sheet // Test add shape with unsupported charset style sheet
f.Styles = nil f.Styles = nil
f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset) f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset)
assert.EqualError(t, f.AddShape("Sheet1", "B30", &Shape{Type: "rect", Paragraph: []RichTextRun{{Text: "Rectangle"}, {}}}), "XML syntax error on line 1: invalid UTF-8") assert.EqualError(t, f.AddShape("Sheet1", &Shape{Cell: "B30", Type: "rect", Paragraph: []RichTextRun{{Text: "Rectangle"}, {}}}), "XML syntax error on line 1: invalid UTF-8")
// Test add shape with unsupported charset content types // Test add shape with unsupported charset content types
f = NewFile() f = NewFile()
f.ContentTypes = nil f.ContentTypes = nil
f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
assert.EqualError(t, f.AddShape("Sheet1", "B30", &Shape{Type: "rect", Paragraph: []RichTextRun{{Text: "Rectangle"}, {}}}), "XML syntax error on line 1: invalid UTF-8") assert.EqualError(t, f.AddShape("Sheet1", &Shape{Cell: "B30", Type: "rect", Paragraph: []RichTextRun{{Text: "Rectangle"}, {}}}), "XML syntax error on line 1: invalid UTF-8")
} }
func TestAddDrawingShape(t *testing.T) { func TestAddDrawingShape(t *testing.T) {

333
sheet.go
View File

@ -1,4 +1,4 @@
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in // this source code is governed by a BSD-style license that can be found in
// the LICENSE file. // the LICENSE file.
// //
@ -7,7 +7,7 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming // Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of // API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.16 or later. // data. This library needs Go version 1.18 or later.
package excelize package excelize
@ -27,7 +27,7 @@ import (
"unicode/utf16" "unicode/utf16"
"unicode/utf8" "unicode/utf8"
"github.com/mohae/deepcopy" "github.com/tiendc/go-deepcopy"
) )
// NewSheet provides the function to create a new sheet by given a worksheet // NewSheet provides the function to create a new sheet by given a worksheet
@ -70,8 +70,8 @@ func (f *File) NewSheet(sheet string) (int, error) {
func (f *File) contentTypesReader() (*xlsxTypes, error) { func (f *File) contentTypesReader() (*xlsxTypes, error) {
if f.ContentTypes == nil { if f.ContentTypes == nil {
f.ContentTypes = new(xlsxTypes) f.ContentTypes = new(xlsxTypes)
f.ContentTypes.Lock() f.ContentTypes.mu.Lock()
defer f.ContentTypes.Unlock() defer f.ContentTypes.mu.Unlock()
if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathContentTypes)))). if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(defaultXMLPathContentTypes)))).
Decode(f.ContentTypes); err != nil && err != io.EOF { Decode(f.ContentTypes); err != nil && err != io.EOF {
return f.ContentTypes, err return f.ContentTypes, err
@ -124,7 +124,8 @@ func (f *File) mergeExpandedCols(ws *xlsxWorksheet) {
Width: ws.Cols.Col[i-1].Width, Width: ws.Cols.Col[i-1].Width,
}, ws.Cols.Col[i]); i++ { }, ws.Cols.Col[i]); i++ {
} }
column := deepcopy.Copy(ws.Cols.Col[left]).(xlsxCol) var column xlsxCol
deepcopy.Copy(&column, ws.Cols.Col[left])
if left < i-1 { if left < i-1 {
column.Max = ws.Cols.Col[i-1].Min column.Max = ws.Cols.Col[i-1].Min
} }
@ -164,10 +165,10 @@ func (f *File) workSheetWriter() {
// reusing buffer // reusing buffer
_ = encoder.Encode(sheet) _ = encoder.Encode(sheet)
f.saveFileList(p.(string), replaceRelationshipsBytes(f.replaceNameSpaceBytes(p.(string), buffer.Bytes()))) f.saveFileList(p.(string), replaceRelationshipsBytes(f.replaceNameSpaceBytes(p.(string), buffer.Bytes())))
ok := f.checked[p.(string)] _, ok := f.checked.Load(p.(string))
if ok { if ok {
f.Sheet.Delete(p.(string)) f.Sheet.Delete(p.(string))
f.checked[p.(string)] = false f.checked.Delete(p.(string))
} }
buffer.Reset() buffer.Reset()
} }
@ -178,36 +179,39 @@ func (f *File) workSheetWriter() {
// trimRow provides a function to trim empty rows. // trimRow provides a function to trim empty rows.
func trimRow(sheetData *xlsxSheetData) []xlsxRow { func trimRow(sheetData *xlsxSheetData) []xlsxRow {
var ( var (
row xlsxRow row xlsxRow
rows []xlsxRow i int
) )
for k, v := range sheetData.Row {
for k := range sheetData.Row {
row = sheetData.Row[k] row = sheetData.Row[k]
if row.C = trimCell(v.C); len(row.C) != 0 || row.hasAttr() { if row = trimCell(row); len(row.C) != 0 || row.hasAttr() {
rows = append(rows, row) sheetData.Row[i] = row
} }
i++
} }
return rows return sheetData.Row[:i]
} }
// trimCell provides a function to trim blank cells which created by fillColumns. // trimCell provides a function to trim blank cells which created by fillColumns.
func trimCell(column []xlsxC) []xlsxC { func trimCell(row xlsxRow) xlsxRow {
column := row.C
rowFull := true rowFull := true
for i := range column { for i := range column {
rowFull = column[i].hasValue() && rowFull rowFull = column[i].hasValue() && rowFull
} }
if rowFull { if rowFull {
return column return row
} }
col := make([]xlsxC, len(column))
i := 0 i := 0
for _, c := range column { for _, c := range column {
if c.hasValue() { if c.hasValue() {
col[i] = c row.C[i] = c
i++ i++
} }
} }
return col[:i] row.C = row.C[:i]
return row
} }
// setContentTypes provides a function to read and update property of contents // setContentTypes provides a function to read and update property of contents
@ -217,8 +221,8 @@ func (f *File) setContentTypes(partName, contentType string) error {
if err != nil { if err != nil {
return err return err
} }
content.Lock() content.mu.Lock()
defer content.Unlock() defer content.mu.Unlock()
content.Overrides = append(content.Overrides, xlsxOverride{ content.Overrides = append(content.Overrides, xlsxOverride{
PartName: partName, PartName: partName,
ContentType: contentType, ContentType: contentType,
@ -237,7 +241,7 @@ func (f *File) setSheet(index int, name string) {
sheetXMLPath := "xl/worksheets/sheet" + strconv.Itoa(index) + ".xml" sheetXMLPath := "xl/worksheets/sheet" + strconv.Itoa(index) + ".xml"
f.sheetMap[name] = sheetXMLPath f.sheetMap[name] = sheetXMLPath
f.Sheet.Store(sheetXMLPath, &ws) f.Sheet.Store(sheetXMLPath, &ws)
f.xmlAttr[sheetXMLPath] = []xml.Attr{NameSpaceSpreadSheet} f.xmlAttr.Store(sheetXMLPath, []xml.Attr{NameSpaceSpreadSheet})
} }
// relsWriter provides a function to save relationships after // relsWriter provides a function to save relationships after
@ -360,7 +364,7 @@ func (f *File) SetSheetName(source, target string) error {
if err = checkSheetName(target); err != nil { if err = checkSheetName(target); err != nil {
return err return err
} }
if strings.EqualFold(target, source) { if target == source {
return err return err
} }
wb, _ := f.workbookReader() wb, _ := f.workbookReader()
@ -371,6 +375,12 @@ func (f *File) SetSheetName(source, target string) error {
delete(f.sheetMap, source) delete(f.sheetMap, source)
} }
} }
if wb.DefinedNames == nil {
return err
}
for i, dn := range wb.DefinedNames.DefinedName {
wb.DefinedNames.DefinedName[i].Data = adjustRangeSheetName(dn.Data, source, target)
}
return err return err
} }
@ -576,14 +586,14 @@ func (f *File) DeleteSheet(sheet string) error {
} }
} }
target := f.deleteSheetFromWorkbookRels(v.ID) target := f.deleteSheetFromWorkbookRels(v.ID)
_ = f.deleteSheetFromContentTypes(target) _ = f.removeContentTypesPart(ContentTypeSpreadSheetMLWorksheet, target)
_ = f.deleteCalcChain(f.getSheetID(sheet), "") _ = f.deleteCalcChain(f.getSheetID(sheet), "")
delete(f.sheetMap, v.Name) delete(f.sheetMap, v.Name)
f.Pkg.Delete(sheetXML) f.Pkg.Delete(sheetXML)
f.Pkg.Delete(rels) f.Pkg.Delete(rels)
f.Relationships.Delete(rels) f.Relationships.Delete(rels)
f.Sheet.Delete(sheetXML) f.Sheet.Delete(sheetXML)
delete(f.xmlAttr, sheetXML) f.xmlAttr.Delete(sheetXML)
f.SheetCount-- f.SheetCount--
} }
index, err := f.GetSheetIndex(activeSheetName) index, err := f.GetSheetIndex(activeSheetName)
@ -591,6 +601,49 @@ func (f *File) DeleteSheet(sheet string) error {
return err return err
} }
// MoveSheet moves a sheet to a specified position in the workbook. The function
// moves the source sheet before the target sheet. After moving, other sheets
// will be shifted to the left or right. If the sheet is already at the target
// position, the function will not perform any action. Not that this function
// will be ungroup all sheets after moving. For example, move Sheet2 before
// Sheet1:
//
// err := f.MoveSheet("Sheet2", "Sheet1")
func (f *File) MoveSheet(source, target string) error {
if strings.EqualFold(source, target) {
return nil
}
wb, err := f.workbookReader()
if err != nil {
return err
}
sourceIdx, err := f.GetSheetIndex(source)
if err != nil {
return err
}
targetIdx, err := f.GetSheetIndex(target)
if err != nil {
return err
}
if sourceIdx < 0 {
return ErrSheetNotExist{source}
}
if targetIdx < 0 {
return ErrSheetNotExist{target}
}
_ = f.UngroupSheets()
activeSheetName := f.GetSheetName(f.GetActiveSheetIndex())
sourceSheet := wb.Sheets.Sheet[sourceIdx]
wb.Sheets.Sheet = append(wb.Sheets.Sheet[:sourceIdx], wb.Sheets.Sheet[sourceIdx+1:]...)
if targetIdx > sourceIdx {
targetIdx--
}
wb.Sheets.Sheet = append(wb.Sheets.Sheet[:targetIdx], append([]xlsxSheet{sourceSheet}, wb.Sheets.Sheet[targetIdx:]...)...)
activeSheetIdx, _ := f.GetSheetIndex(activeSheetName)
f.SetActiveSheet(activeSheetIdx)
return err
}
// deleteAndAdjustDefinedNames delete and adjust defined name in the workbook // deleteAndAdjustDefinedNames delete and adjust defined name in the workbook
// by given worksheet ID. // by given worksheet ID.
func deleteAndAdjustDefinedNames(wb *xlsxWorkbook, deleteLocalSheetID int) { func deleteAndAdjustDefinedNames(wb *xlsxWorkbook, deleteLocalSheetID int) {
@ -615,8 +668,8 @@ func deleteAndAdjustDefinedNames(wb *xlsxWorkbook, deleteLocalSheetID int) {
// relationships by given relationships ID in the file workbook.xml.rels. // relationships by given relationships ID in the file workbook.xml.rels.
func (f *File) deleteSheetFromWorkbookRels(rID string) string { func (f *File) deleteSheetFromWorkbookRels(rID string) string {
rels, _ := f.relsReader(f.getWorkbookRelsPath()) rels, _ := f.relsReader(f.getWorkbookRelsPath())
rels.Lock() rels.mu.Lock()
defer rels.Unlock() defer rels.mu.Unlock()
for k, v := range rels.Relationships { for k, v := range rels.Relationships {
if v.ID == rID { if v.ID == rID {
rels.Relationships = append(rels.Relationships[:k], rels.Relationships[k+1:]...) rels.Relationships = append(rels.Relationships[:k], rels.Relationships[k+1:]...)
@ -626,24 +679,50 @@ func (f *File) deleteSheetFromWorkbookRels(rID string) string {
return "" return ""
} }
// deleteSheetFromContentTypes provides a function to remove worksheet // deleteSheetRelationships provides a function to delete relationships in
// relationships by given target name in the file [Content_Types].xml. // xl/worksheets/_rels/sheet%d.xml.rels by given worksheet name and
func (f *File) deleteSheetFromContentTypes(target string) error { // relationship index.
if !strings.HasPrefix(target, "/") { func (f *File) deleteSheetRelationships(sheet, rID string) {
target = "/xl/" + target name, ok := f.getSheetXMLPath(sheet)
if !ok {
name = strings.ToLower(sheet) + ".xml"
} }
content, err := f.contentTypesReader() rels := "xl/worksheets/_rels/" + strings.TrimPrefix(name, "xl/worksheets/") + ".rels"
if err != nil { sheetRels, _ := f.relsReader(rels)
return err if sheetRels == nil {
sheetRels = &xlsxRelationships{}
} }
content.Lock() sheetRels.mu.Lock()
defer content.Unlock() defer sheetRels.mu.Unlock()
for k, v := range content.Overrides { for k, v := range sheetRels.Relationships {
if v.PartName == target { if v.ID == rID {
content.Overrides = append(content.Overrides[:k], content.Overrides[k+1:]...) sheetRels.Relationships = append(sheetRels.Relationships[:k], sheetRels.Relationships[k+1:]...)
} }
} }
return err f.Relationships.Store(rels, sheetRels)
}
// getSheetRelationshipsTargetByID provides a function to get Target attribute
// value in xl/worksheets/_rels/sheet%d.xml.rels by given worksheet name and
// relationship index.
func (f *File) getSheetRelationshipsTargetByID(sheet, rID string) string {
name, ok := f.getSheetXMLPath(sheet)
if !ok {
name = strings.ToLower(sheet) + ".xml"
}
rels := "xl/worksheets/_rels/" + strings.TrimPrefix(name, "xl/worksheets/") + ".rels"
sheetRels, _ := f.relsReader(rels)
if sheetRels == nil {
sheetRels = &xlsxRelationships{}
}
sheetRels.mu.Lock()
defer sheetRels.mu.Unlock()
for _, v := range sheetRels.Relationships {
if v.ID == rID {
return v.Target
}
}
return ""
} }
// CopySheet provides a function to duplicate a worksheet by gave source and // CopySheet provides a function to duplicate a worksheet by gave source and
@ -672,7 +751,8 @@ func (f *File) copySheet(from, to int) error {
if err != nil { if err != nil {
return err return err
} }
worksheet := deepcopy.Copy(sheet).(*xlsxWorksheet) worksheet := &xlsxWorksheet{}
deepcopy.Copy(worksheet, sheet)
toSheetID := strconv.Itoa(f.getSheetID(f.GetSheetName(to))) toSheetID := strconv.Itoa(f.getSheetID(f.GetSheetName(to)))
sheetXMLPath := "xl/worksheets/sheet" + toSheetID + ".xml" sheetXMLPath := "xl/worksheets/sheet" + toSheetID + ".xml"
if len(worksheet.SheetViews.SheetView) > 0 { if len(worksheet.SheetViews.SheetView) > 0 {
@ -688,8 +768,8 @@ func (f *File) copySheet(from, to int) error {
f.Pkg.Store(toRels, rels.([]byte)) f.Pkg.Store(toRels, rels.([]byte))
} }
fromSheetXMLPath, _ := f.getSheetXMLPath(fromSheet) fromSheetXMLPath, _ := f.getSheetXMLPath(fromSheet)
fromSheetAttr := f.xmlAttr[fromSheetXMLPath] fromSheetAttr, _ := f.xmlAttr.Load(fromSheetXMLPath)
f.xmlAttr[sheetXMLPath] = fromSheetAttr f.xmlAttr.Store(sheetXMLPath, fromSheetAttr)
return err return err
} }
@ -738,6 +818,11 @@ func (f *File) SetSheetVisible(sheet string, visible bool, veryHidden ...bool) e
return err return err
} }
tabSelected := false tabSelected := false
if ws.SheetViews == nil {
ws.SheetViews = &xlsxSheetViews{
SheetView: []xlsxSheetView{{WorkbookViewID: 0}},
}
}
if len(ws.SheetViews.SheetView) > 0 { if len(ws.SheetViews.SheetView) > 0 {
tabSelected = ws.SheetViews.SheetView[0].TabSelected tabSelected = ws.SheetViews.SheetView[0].TabSelected
} }
@ -772,7 +857,7 @@ func (ws *xlsxWorksheet) setPanes(panes *Panes) error {
} }
} }
var s []*xlsxSelection var s []*xlsxSelection
for _, p := range panes.Panes { for _, p := range panes.Selection {
s = append(s, &xlsxSelection{ s = append(s, &xlsxSelection{
ActiveCell: p.ActiveCell, ActiveCell: p.ActiveCell,
Pane: p.Pane, Pane: p.Pane,
@ -859,7 +944,7 @@ func (ws *xlsxWorksheet) setPanes(panes *Panes) error {
// YSplit: 0, // YSplit: 0,
// TopLeftCell: "B1", // TopLeftCell: "B1",
// ActivePane: "topRight", // ActivePane: "topRight",
// Panes: []excelize.PaneOptions{ // Selection: []excelize.Selection{
// {SQRef: "K16", ActiveCell: "K16", Pane: "topRight"}, // {SQRef: "K16", ActiveCell: "K16", Pane: "topRight"},
// }, // },
// }) // })
@ -874,7 +959,7 @@ func (ws *xlsxWorksheet) setPanes(panes *Panes) error {
// YSplit: 9, // YSplit: 9,
// TopLeftCell: "A34", // TopLeftCell: "A34",
// ActivePane: "bottomLeft", // ActivePane: "bottomLeft",
// Panes: []excelize.PaneOptions{ // Selection: []excelize.Selection{
// {SQRef: "A11:XFD11", ActiveCell: "A11", Pane: "bottomLeft"}, // {SQRef: "A11:XFD11", ActiveCell: "A11", Pane: "bottomLeft"},
// }, // },
// }) // })
@ -889,7 +974,7 @@ func (ws *xlsxWorksheet) setPanes(panes *Panes) error {
// YSplit: 1800, // YSplit: 1800,
// TopLeftCell: "N57", // TopLeftCell: "N57",
// ActivePane: "bottomLeft", // ActivePane: "bottomLeft",
// Panes: []excelize.PaneOptions{ // Selection: []excelize.Selection{
// {SQRef: "I36", ActiveCell: "I36"}, // {SQRef: "I36", ActiveCell: "I36"},
// {SQRef: "G33", ActiveCell: "G33", Pane: "topRight"}, // {SQRef: "G33", ActiveCell: "G33", Pane: "topRight"},
// {SQRef: "J60", ActiveCell: "J60", Pane: "bottomLeft"}, // {SQRef: "J60", ActiveCell: "J60", Pane: "bottomLeft"},
@ -908,6 +993,50 @@ func (f *File) SetPanes(sheet string, panes *Panes) error {
return ws.setPanes(panes) return ws.setPanes(panes)
} }
// getPanes returns freeze panes, split panes, and views of the worksheet.
func (ws *xlsxWorksheet) getPanes() Panes {
var (
panes Panes
section []Selection
)
if ws.SheetViews == nil || len(ws.SheetViews.SheetView) < 1 {
return panes
}
sw := ws.SheetViews.SheetView[len(ws.SheetViews.SheetView)-1]
for _, s := range sw.Selection {
if s != nil {
section = append(section, Selection{
SQRef: s.SQRef,
ActiveCell: s.ActiveCell,
Pane: s.Pane,
})
}
}
panes.Selection = section
if sw.Pane == nil {
return panes
}
panes.ActivePane = sw.Pane.ActivePane
if sw.Pane.State == "frozen" {
panes.Freeze = true
}
panes.TopLeftCell = sw.Pane.TopLeftCell
panes.XSplit = int(sw.Pane.XSplit)
panes.YSplit = int(sw.Pane.YSplit)
return panes
}
// GetPanes provides a function to get freeze panes, split panes, and worksheet
// views by given worksheet name.
func (f *File) GetPanes(sheet string) (Panes, error) {
var panes Panes
ws, err := f.workSheetReader(sheet)
if err != nil {
return panes, err
}
return ws.getPanes(), err
}
// GetSheetVisible provides a function to get worksheet visible by given worksheet // GetSheetVisible provides a function to get worksheet visible by given worksheet
// name. For example, get visible state of Sheet1: // name. For example, get visible state of Sheet1:
// //
@ -977,6 +1106,7 @@ func (f *File) searchSheet(name, value string, regSearch bool) (result []string,
if sst, err = f.sharedStringsReader(); err != nil { if sst, err = f.sharedStringsReader(); err != nil {
return return
} }
regex := regexp.MustCompile(value)
decoder := f.xmlNewDecoder(bytes.NewReader(f.readBytes(name))) decoder := f.xmlNewDecoder(bytes.NewReader(f.readBytes(name)))
for { for {
var token xml.Token var token xml.Token
@ -1001,7 +1131,6 @@ func (f *File) searchSheet(name, value string, regSearch bool) (result []string,
_ = decoder.DecodeElement(&colCell, &xmlElement) _ = decoder.DecodeElement(&colCell, &xmlElement)
val, _ := colCell.getValueFrom(f, sst, false) val, _ := colCell.getValueFrom(f, sst, false)
if regSearch { if regSearch {
regex := regexp.MustCompile(value)
if !regex.MatchString(val) { if !regex.MatchString(val) {
continue continue
} }
@ -1079,8 +1208,8 @@ func attrValToBool(name string, attrs []xml.Attr) (val bool, err error) {
// DifferentFirst | Different first-page header and footer indicator // DifferentFirst | Different first-page header and footer indicator
// DifferentOddEven | Different odd and even page headers and footers indicator // DifferentOddEven | Different odd and even page headers and footers indicator
// ScaleWithDoc | Scale header and footer with document scaling // ScaleWithDoc | Scale header and footer with document scaling
// OddFooter | Odd Page Footer // OddFooter | Odd Page Footer, or primary Page Footer if 'DifferentOddEven' is 'false'
// OddHeader | Odd Header // OddHeader | Odd Header, or primary Page Header if 'DifferentOddEven' is 'false'
// EvenFooter | Even Page Footer // EvenFooter | Even Page Footer
// EvenHeader | Even Page Header // EvenHeader | Even Page Header
// FirstFooter | First Page Footer // FirstFooter | First Page Footer
@ -1112,7 +1241,7 @@ func attrValToBool(name string, attrs []xml.Attr) (val bool, err error) {
// | // |
// &F | Current workbook's file name // &F | Current workbook's file name
// | // |
// &G | Drawing object as background (Not support currently) // &G | Drawing object as background (Use AddHeaderFooterImage)
// | // |
// &H | Shadow text format // &H | Shadow text format
// | // |
@ -1216,6 +1345,32 @@ func (f *File) SetHeaderFooter(sheet string, opts *HeaderFooterOptions) error {
return err return err
} }
// GetHeaderFooter provides a function to get worksheet header and footer by
// given worksheet name.
func (f *File) GetHeaderFooter(sheet string) (*HeaderFooterOptions, error) {
var opts *HeaderFooterOptions
ws, err := f.workSheetReader(sheet)
if err != nil {
return opts, err
}
if ws.HeaderFooter == nil {
return opts, err
}
opts = &HeaderFooterOptions{
AlignWithMargins: ws.HeaderFooter.AlignWithMargins,
DifferentFirst: ws.HeaderFooter.DifferentFirst,
DifferentOddEven: ws.HeaderFooter.DifferentOddEven,
ScaleWithDoc: ws.HeaderFooter.ScaleWithDoc,
OddHeader: ws.HeaderFooter.OddHeader,
OddFooter: ws.HeaderFooter.OddFooter,
EvenHeader: ws.HeaderFooter.EvenHeader,
EvenFooter: ws.HeaderFooter.EvenFooter,
FirstHeader: ws.HeaderFooter.FirstHeader,
FirstFooter: ws.HeaderFooter.FirstFooter,
}
return opts, err
}
// ProtectSheet provides a function to prevent other users from accidentally or // ProtectSheet provides a function to prevent other users from accidentally or
// deliberately changing, moving, or deleting data in a worksheet. The // deliberately changing, moving, or deleting data in a worksheet. The
// optional field AlgorithmName specified hash algorithm, support XOR, MD4, // optional field AlgorithmName specified hash algorithm, support XOR, MD4,
@ -1456,8 +1611,7 @@ func (f *File) SetPageLayout(sheet string, opts *PageLayoutOptions) error {
if opts == nil { if opts == nil {
return err return err
} }
ws.setPageSetUp(opts) return ws.setPageSetUp(opts)
return err
} }
// newPageSetUp initialize page setup settings for the worksheet if which not // newPageSetUp initialize page setup settings for the worksheet if which not
@ -1469,12 +1623,15 @@ func (ws *xlsxWorksheet) newPageSetUp() {
} }
// setPageSetUp set page setup settings for the worksheet by given options. // setPageSetUp set page setup settings for the worksheet by given options.
func (ws *xlsxWorksheet) setPageSetUp(opts *PageLayoutOptions) { func (ws *xlsxWorksheet) setPageSetUp(opts *PageLayoutOptions) error {
if opts.Size != nil { if opts.Size != nil {
ws.newPageSetUp() ws.newPageSetUp()
ws.PageSetUp.PaperSize = opts.Size ws.PageSetUp.PaperSize = opts.Size
} }
if opts.Orientation != nil && (*opts.Orientation == "portrait" || *opts.Orientation == "landscape") { if opts.Orientation != nil {
if inStrSlice(supportedPageOrientation, *opts.Orientation, true) == -1 {
return newInvalidPageLayoutValueError("Orientation", *opts.Orientation, strings.Join(supportedPageOrientation, ", "))
}
ws.newPageSetUp() ws.newPageSetUp()
ws.PageSetUp.Orientation = *opts.Orientation ws.PageSetUp.Orientation = *opts.Orientation
} }
@ -1483,7 +1640,10 @@ func (ws *xlsxWorksheet) setPageSetUp(opts *PageLayoutOptions) {
ws.PageSetUp.FirstPageNumber = strconv.Itoa(int(*opts.FirstPageNumber)) ws.PageSetUp.FirstPageNumber = strconv.Itoa(int(*opts.FirstPageNumber))
ws.PageSetUp.UseFirstPageNumber = true ws.PageSetUp.UseFirstPageNumber = true
} }
if opts.AdjustTo != nil && 10 <= *opts.AdjustTo && *opts.AdjustTo <= 400 { if opts.AdjustTo != nil {
if *opts.AdjustTo < 10 || 400 < *opts.AdjustTo {
return ErrPageSetupAdjustTo
}
ws.newPageSetUp() ws.newPageSetUp()
ws.PageSetUp.Scale = int(*opts.AdjustTo) ws.PageSetUp.Scale = int(*opts.AdjustTo)
} }
@ -1499,13 +1659,21 @@ func (ws *xlsxWorksheet) setPageSetUp(opts *PageLayoutOptions) {
ws.newPageSetUp() ws.newPageSetUp()
ws.PageSetUp.BlackAndWhite = *opts.BlackAndWhite ws.PageSetUp.BlackAndWhite = *opts.BlackAndWhite
} }
if opts.PageOrder != nil {
if inStrSlice(supportedPageOrder, *opts.PageOrder, true) == -1 {
return newInvalidPageLayoutValueError("PageOrder", *opts.PageOrder, strings.Join(supportedPageOrder, ", "))
}
ws.newPageSetUp()
ws.PageSetUp.PageOrder = *opts.PageOrder
}
return nil
} }
// GetPageLayout provides a function to gets worksheet page layout. // GetPageLayout provides a function to gets worksheet page layout.
func (f *File) GetPageLayout(sheet string) (PageLayoutOptions, error) { func (f *File) GetPageLayout(sheet string) (PageLayoutOptions, error) {
opts := PageLayoutOptions{ opts := PageLayoutOptions{
Size: intPtr(0), Size: intPtr(0),
Orientation: stringPtr("portrait"), Orientation: stringPtr(supportedPageOrientation[0]),
FirstPageNumber: uintPtr(1), FirstPageNumber: uintPtr(1),
AdjustTo: uintPtr(100), AdjustTo: uintPtr(100),
} }
@ -1533,6 +1701,9 @@ func (f *File) GetPageLayout(sheet string) (PageLayoutOptions, error) {
opts.FitToWidth = ws.PageSetUp.FitToWidth opts.FitToWidth = ws.PageSetUp.FitToWidth
} }
opts.BlackAndWhite = boolPtr(ws.PageSetUp.BlackAndWhite) opts.BlackAndWhite = boolPtr(ws.PageSetUp.BlackAndWhite)
if ws.PageSetUp.PageOrder != "" {
opts.PageOrder = stringPtr(ws.PageSetUp.PageOrder)
}
} }
return opts, err return opts, err
} }
@ -1547,10 +1718,31 @@ func (f *File) GetPageLayout(sheet string) (PageLayoutOptions, error) {
// Comment: "defined name comment", // Comment: "defined name comment",
// Scope: "Sheet2", // Scope: "Sheet2",
// }) // })
//
// If you fill the RefersTo property with only one columns range without a
// comma, it will work as "Columns to repeat at left" only. For example:
//
// err := f.SetDefinedName(&excelize.DefinedName{
// Name: "_xlnm.Print_Titles",
// RefersTo: "Sheet1!$A:$A",
// Scope: "Sheet1",
// })
//
// If you fill the RefersTo property with only one rows range without a comma,
// it will work as "Rows to repeat at top" only. For example:
//
// err := f.SetDefinedName(&excelize.DefinedName{
// Name: "_xlnm.Print_Titles",
// RefersTo: "Sheet1!$1:$1",
// Scope: "Sheet1",
// })
func (f *File) SetDefinedName(definedName *DefinedName) error { func (f *File) SetDefinedName(definedName *DefinedName) error {
if definedName.Name == "" || definedName.RefersTo == "" { if definedName.Name == "" || definedName.RefersTo == "" {
return ErrParameterInvalid return ErrParameterInvalid
} }
if err := checkDefinedName(definedName.Name); err != nil && inStrSlice(builtInDefinedNames[:2], definedName.Name, false) == -1 {
return err
}
wb, err := f.workbookReader() wb, err := f.workbookReader()
if err != nil { if err != nil {
return err return err
@ -1666,11 +1858,8 @@ func (f *File) GroupSheets(sheets []string) error {
} }
for _, ws := range wss { for _, ws := range wss {
sheetViews := ws.SheetViews.SheetView sheetViews := ws.SheetViews.SheetView
if len(sheetViews) > 0 { for idx := range sheetViews {
for idx := range sheetViews { ws.SheetViews.SheetView[idx].TabSelected = true
ws.SheetViews.SheetView[idx].TabSelected = true
}
continue
} }
} }
return nil return nil
@ -1685,10 +1874,8 @@ func (f *File) UngroupSheets() error {
} }
ws, _ := f.workSheetReader(sheet) ws, _ := f.workSheetReader(sheet)
sheetViews := ws.SheetViews.SheetView sheetViews := ws.SheetViews.SheetView
if len(sheetViews) > 0 { for idx := range sheetViews {
for idx := range sheetViews { ws.SheetViews.SheetView[idx].TabSelected = false
ws.SheetViews.SheetView[idx].TabSelected = false
}
} }
} }
return nil return nil
@ -1817,7 +2004,7 @@ func (f *File) RemovePageBreak(sheet, cell string) error {
} }
// relsReader provides a function to get the pointer to the structure // relsReader provides a function to get the pointer to the structure
// after deserialization of xl/worksheets/_rels/sheet%d.xml.rels. // after deserialization of relationships parts.
func (f *File) relsReader(path string) (*xlsxRelationships, error) { func (f *File) relsReader(path string) (*xlsxRelationships, error) {
rels, _ := f.Relationships.Load(path) rels, _ := f.Relationships.Load(path)
if rels == nil { if rels == nil {
@ -1839,9 +2026,7 @@ func (f *File) relsReader(path string) (*xlsxRelationships, error) {
// fillSheetData ensures there are enough rows, and columns in the chosen // fillSheetData ensures there are enough rows, and columns in the chosen
// row to accept data. Missing rows are backfilled and given their row number // row to accept data. Missing rows are backfilled and given their row number
// Uses the last populated row as a hint for the size of the next row to add // Uses the last populated row as a hint for the size of the next row to add
func prepareSheetXML(ws *xlsxWorksheet, col int, row int) { func (ws *xlsxWorksheet) prepareSheetXML(col int, row int) {
ws.Lock()
defer ws.Unlock()
rowCount := len(ws.SheetData.Row) rowCount := len(ws.SheetData.Row)
sizeHint := 0 sizeHint := 0
var ht *float64 var ht *float64
@ -1875,9 +2060,7 @@ func fillColumns(rowData *xlsxRow, col, row int) {
} }
// makeContiguousColumns make columns in specific rows as contiguous. // makeContiguousColumns make columns in specific rows as contiguous.
func makeContiguousColumns(ws *xlsxWorksheet, fromRow, toRow, colCount int) { func (ws *xlsxWorksheet) makeContiguousColumns(fromRow, toRow, colCount int) {
ws.Lock()
defer ws.Unlock()
for ; fromRow < toRow; fromRow++ { for ; fromRow < toRow; fromRow++ {
rowData := &ws.SheetData.Row[fromRow-1] rowData := &ws.SheetData.Row[fromRow-1]
fillColumns(rowData, colCount, fromRow) fillColumns(rowData, colCount, fromRow)
@ -1915,7 +2098,7 @@ func (f *File) SetSheetDimension(sheet string, rangeRef string) error {
return err return err
} }
_ = sortCoordinates(coordinates) _ = sortCoordinates(coordinates)
ref, err := f.coordinatesToRangeRef(coordinates) ref, err := coordinatesToRangeRef(coordinates)
ws.Dimension = &xlsxDimension{Ref: ref} ws.Dimension = &xlsxDimension{Ref: ref}
return err return err
} }

View File

@ -8,6 +8,7 @@ import (
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"sync"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -37,25 +38,29 @@ func TestNewSheet(t *testing.T) {
assert.Equal(t, -1, sheetID) assert.Equal(t, -1, sheetID)
} }
func TestSetPanes(t *testing.T) { func TestPanes(t *testing.T) {
f := NewFile() f := NewFile()
assert.NoError(t, f.SetPanes("Sheet1", &Panes{Freeze: false, Split: false})) assert.NoError(t, f.SetPanes("Sheet1", &Panes{Freeze: false, Split: false}))
_, err := f.NewSheet("Panes 2") _, err := f.NewSheet("Panes 2")
assert.NoError(t, err) assert.NoError(t, err)
assert.NoError(t, f.SetPanes("Panes 2",
&Panes{ expected := Panes{
Freeze: true, Freeze: true,
Split: false, Split: false,
XSplit: 1, XSplit: 1,
YSplit: 0, YSplit: 0,
TopLeftCell: "B1", TopLeftCell: "B1",
ActivePane: "topRight", ActivePane: "topRight",
Panes: []PaneOptions{ Selection: []Selection{
{SQRef: "K16", ActiveCell: "K16", Pane: "topRight"}, {SQRef: "K16", ActiveCell: "K16", Pane: "topRight"},
},
}, },
)) }
assert.NoError(t, f.SetPanes("Panes 2", &expected))
panes, err := f.GetPanes("Panes 2")
assert.NoError(t, err)
assert.Equal(t, expected, panes)
_, err = f.NewSheet("Panes 3") _, err = f.NewSheet("Panes 3")
assert.NoError(t, err) assert.NoError(t, err)
assert.NoError(t, f.SetPanes("Panes 3", assert.NoError(t, f.SetPanes("Panes 3",
@ -66,7 +71,7 @@ func TestSetPanes(t *testing.T) {
YSplit: 1800, YSplit: 1800,
TopLeftCell: "N57", TopLeftCell: "N57",
ActivePane: "bottomLeft", ActivePane: "bottomLeft",
Panes: []PaneOptions{ Selection: []Selection{
{SQRef: "I36", ActiveCell: "I36"}, {SQRef: "I36", ActiveCell: "I36"},
{SQRef: "G33", ActiveCell: "G33", Pane: "topRight"}, {SQRef: "G33", ActiveCell: "G33", Pane: "topRight"},
{SQRef: "J60", ActiveCell: "J60", Pane: "bottomLeft"}, {SQRef: "J60", ActiveCell: "J60", Pane: "bottomLeft"},
@ -84,7 +89,7 @@ func TestSetPanes(t *testing.T) {
YSplit: 9, YSplit: 9,
TopLeftCell: "A34", TopLeftCell: "A34",
ActivePane: "bottomLeft", ActivePane: "bottomLeft",
Panes: []PaneOptions{ Selection: []Selection{
{SQRef: "A11:XFD11", ActiveCell: "A11", Pane: "bottomLeft"}, {SQRef: "A11:XFD11", ActiveCell: "A11", Pane: "bottomLeft"},
}, },
}, },
@ -94,9 +99,29 @@ func TestSetPanes(t *testing.T) {
// Test set panes with invalid sheet name // Test set panes with invalid sheet name
assert.EqualError(t, f.SetPanes("Sheet:1", &Panes{Freeze: false, Split: false}), ErrSheetNameInvalid.Error()) assert.EqualError(t, f.SetPanes("Sheet:1", &Panes{Freeze: false, Split: false}), ErrSheetNameInvalid.Error())
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetPane.xlsx"))) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetPane.xlsx")))
// Test get panes with empty sheet views
f = NewFile()
ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
assert.True(t, ok)
ws.(*xlsxWorksheet).SheetViews = &xlsxSheetViews{}
_, err = f.GetPanes("Sheet1")
assert.NoError(t, err)
// Test get panes without panes
ws.(*xlsxWorksheet).SheetViews = &xlsxSheetViews{SheetView: []xlsxSheetView{{}}}
_, err = f.GetPanes("Sheet1")
assert.NoError(t, err)
// Test get panes without sheet views
ws.(*xlsxWorksheet).SheetViews = nil
_, err = f.GetPanes("Sheet1")
assert.NoError(t, err)
// Test get panes on not exists worksheet
_, err = f.GetPanes("SheetN")
assert.EqualError(t, err, "sheet SheetN does not exist")
// Test add pane on empty sheet views worksheet // Test add pane on empty sheet views worksheet
f = NewFile() f = NewFile()
f.checked = nil f.checked = sync.Map{}
f.Sheet.Delete("xl/worksheets/sheet1.xml") f.Sheet.Delete("xl/worksheets/sheet1.xml")
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData/></worksheet>`)) f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData/></worksheet>`))
assert.NoError(t, f.SetPanes("Sheet1", assert.NoError(t, f.SetPanes("Sheet1",
@ -107,7 +132,7 @@ func TestSetPanes(t *testing.T) {
YSplit: 0, YSplit: 0,
TopLeftCell: "B1", TopLeftCell: "B1",
ActivePane: "topRight", ActivePane: "topRight",
Panes: []PaneOptions{ Selection: []Selection{
{SQRef: "K16", ActiveCell: "K16", Pane: "topRight"}, {SQRef: "K16", ActiveCell: "K16", Pane: "topRight"},
}, },
}, },
@ -149,19 +174,19 @@ func TestSearchSheet(t *testing.T) {
f = NewFile() f = NewFile()
f.Sheet.Delete("xl/worksheets/sheet1.xml") f.Sheet.Delete("xl/worksheets/sheet1.xml")
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`<worksheet><sheetData><row r="A"><c r="2" t="inlineStr"><is><t>A</t></is></c></row></sheetData></worksheet>`)) f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`<worksheet><sheetData><row r="A"><c r="2" t="inlineStr"><is><t>A</t></is></c></row></sheetData></worksheet>`))
f.checked = nil f.checked = sync.Map{}
result, err = f.SearchSheet("Sheet1", "A") result, err = f.SearchSheet("Sheet1", "A")
assert.EqualError(t, err, "strconv.Atoi: parsing \"A\": invalid syntax") assert.EqualError(t, err, "strconv.Atoi: parsing \"A\": invalid syntax")
assert.Equal(t, []string(nil), result) assert.Equal(t, []string(nil), result)
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`<worksheet><sheetData><row r="2"><c r="A" t="inlineStr"><is><t>A</t></is></c></row></sheetData></worksheet>`)) f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`<worksheet><sheetData><row r="2"><c r="A" t="inlineStr"><is><t>A</t></is></c></row></sheetData></worksheet>`))
result, err = f.SearchSheet("Sheet1", "A") result, err = f.SearchSheet("Sheet1", "A")
assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), err)
assert.Equal(t, []string(nil), result) assert.Equal(t, []string(nil), result)
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`<worksheet><sheetData><row r="0"><c r="A1" t="inlineStr"><is><t>A</t></is></c></row></sheetData></worksheet>`)) f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(`<worksheet><sheetData><row r="0"><c r="A1" t="inlineStr"><is><t>A</t></is></c></row></sheetData></worksheet>`))
result, err = f.SearchSheet("Sheet1", "A") result, err = f.SearchSheet("Sheet1", "A")
assert.EqualError(t, err, "invalid cell reference [1, 0]") assert.Equal(t, newCoordinatesToCellNameError(1, 0), err)
assert.Equal(t, []string(nil), result) assert.Equal(t, []string(nil), result)
// Test search sheet with unsupported charset shared strings table // Test search sheet with unsupported charset shared strings table
@ -185,6 +210,7 @@ func TestSetPageLayout(t *testing.T) {
FitToHeight: intPtr(2), FitToHeight: intPtr(2),
FitToWidth: intPtr(2), FitToWidth: intPtr(2),
BlackAndWhite: boolPtr(true), BlackAndWhite: boolPtr(true),
PageOrder: stringPtr("overThenDown"),
} }
assert.NoError(t, f.SetPageLayout("Sheet1", &expected)) assert.NoError(t, f.SetPageLayout("Sheet1", &expected))
opts, err := f.GetPageLayout("Sheet1") opts, err := f.GetPageLayout("Sheet1")
@ -194,6 +220,16 @@ func TestSetPageLayout(t *testing.T) {
assert.EqualError(t, f.SetPageLayout("SheetN", nil), "sheet SheetN does not exist") assert.EqualError(t, f.SetPageLayout("SheetN", nil), "sheet SheetN does not exist")
// Test set page layout with invalid sheet name // Test set page layout with invalid sheet name
assert.EqualError(t, f.SetPageLayout("Sheet:1", nil), ErrSheetNameInvalid.Error()) assert.EqualError(t, f.SetPageLayout("Sheet:1", nil), ErrSheetNameInvalid.Error())
// Test set page layout with invalid parameters
assert.EqualError(t, f.SetPageLayout("Sheet1", &PageLayoutOptions{
AdjustTo: uintPtr(5),
}), "adjust to value must be between 10 and 400")
assert.EqualError(t, f.SetPageLayout("Sheet1", &PageLayoutOptions{
Orientation: stringPtr("x"),
}), "invalid Orientation value \"x\", acceptable value should be one of portrait, landscape")
assert.EqualError(t, f.SetPageLayout("Sheet1", &PageLayoutOptions{
PageOrder: stringPtr("x"),
}), "invalid PageOrder value \"x\", acceptable value should be one of overThenDown, downThenOver")
} }
func TestGetPageLayout(t *testing.T) { func TestGetPageLayout(t *testing.T) {
@ -206,8 +242,16 @@ func TestGetPageLayout(t *testing.T) {
assert.EqualError(t, err, ErrSheetNameInvalid.Error()) assert.EqualError(t, err, ErrSheetNameInvalid.Error())
} }
func TestSetHeaderFooter(t *testing.T) { func TestHeaderFooter(t *testing.T) {
f := NewFile() f := NewFile()
// Test get header and footer with default header and footer settings
opts, err := f.GetHeaderFooter("Sheet1")
assert.NoError(t, err)
assert.Equal(t, (*HeaderFooterOptions)(nil), opts)
// Test get header and footer on not exists worksheet
_, err = f.GetHeaderFooter("SheetN")
assert.EqualError(t, err, "sheet SheetN does not exist")
assert.NoError(t, f.SetCellStr("Sheet1", "A1", "Test SetHeaderFooter")) assert.NoError(t, f.SetCellStr("Sheet1", "A1", "Test SetHeaderFooter"))
// Test set header and footer on not exists worksheet // Test set header and footer on not exists worksheet
assert.EqualError(t, f.SetHeaderFooter("SheetN", nil), "sheet SheetN does not exist") assert.EqualError(t, f.SetHeaderFooter("SheetN", nil), "sheet SheetN does not exist")
@ -227,7 +271,7 @@ func TestSetHeaderFooter(t *testing.T) {
EvenFooter: text, EvenFooter: text,
FirstHeader: text, FirstHeader: text,
})) }))
assert.NoError(t, f.SetHeaderFooter("Sheet1", &HeaderFooterOptions{ expected := &HeaderFooterOptions{
DifferentFirst: true, DifferentFirst: true,
DifferentOddEven: true, DifferentOddEven: true,
OddHeader: "&R&P", OddHeader: "&R&P",
@ -235,14 +279,18 @@ func TestSetHeaderFooter(t *testing.T) {
EvenHeader: "&L&P", EvenHeader: "&L&P",
EvenFooter: "&L&D&R&T", EvenFooter: "&L&D&R&T",
FirstHeader: `&CCenter &"-,Bold"Bold&"-,Regular"HeaderU+000A&D`, FirstHeader: `&CCenter &"-,Bold"Bold&"-,Regular"HeaderU+000A&D`,
})) }
assert.NoError(t, f.SetHeaderFooter("Sheet1", expected))
opts, err = f.GetHeaderFooter("Sheet1")
assert.NoError(t, err)
assert.Equal(t, expected, opts)
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetHeaderFooter.xlsx"))) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestSetHeaderFooter.xlsx")))
} }
func TestDefinedName(t *testing.T) { func TestDefinedName(t *testing.T) {
f := NewFile() f := NewFile()
assert.NoError(t, f.SetDefinedName(&DefinedName{ assert.NoError(t, f.SetDefinedName(&DefinedName{
Name: "Amount", Name: "Amount.",
RefersTo: "Sheet1!$A$2:$D$5", RefersTo: "Sheet1!$A$2:$D$5",
Comment: "defined name comment", Comment: "defined name comment",
Scope: "Sheet1", Scope: "Sheet1",
@ -252,6 +300,16 @@ func TestDefinedName(t *testing.T) {
RefersTo: "Sheet1!$A$2:$D$5", RefersTo: "Sheet1!$A$2:$D$5",
Comment: "defined name comment", Comment: "defined name comment",
})) }))
assert.NoError(t, f.SetDefinedName(&DefinedName{
Name: builtInDefinedNames[0],
RefersTo: "Sheet1!$A$1:$Z$100",
Scope: "Sheet1",
}))
assert.NoError(t, f.SetDefinedName(&DefinedName{
Name: builtInDefinedNames[1],
RefersTo: "Sheet1!$A:$A,Sheet1!$1:$1",
Scope: "Sheet1",
}))
assert.EqualError(t, f.SetDefinedName(&DefinedName{ assert.EqualError(t, f.SetDefinedName(&DefinedName{
Name: "Amount", Name: "Amount",
RefersTo: "Sheet1!$A$2:$D$5", RefersTo: "Sheet1!$A$2:$D$5",
@ -273,7 +331,7 @@ func TestDefinedName(t *testing.T) {
Name: "Amount", Name: "Amount",
})) }))
assert.Exactly(t, "Sheet1!$A$2:$D$5", f.GetDefinedName()[0].RefersTo) assert.Exactly(t, "Sheet1!$A$2:$D$5", f.GetDefinedName()[0].RefersTo)
assert.Len(t, f.GetDefinedName(), 1) assert.Len(t, f.GetDefinedName(), 3)
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDefinedName.xlsx"))) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDefinedName.xlsx")))
// Test set defined name with unsupported charset workbook // Test set defined name with unsupported charset workbook
f.WorkBook = nil f.WorkBook = nil
@ -418,8 +476,30 @@ func TestSetSheetName(t *testing.T) {
// Test set worksheet with the same name // Test set worksheet with the same name
assert.NoError(t, f.SetSheetName("Sheet1", "Sheet1")) assert.NoError(t, f.SetSheetName("Sheet1", "Sheet1"))
assert.Equal(t, "Sheet1", f.GetSheetName(0)) assert.Equal(t, "Sheet1", f.GetSheetName(0))
// Test set worksheet with the different name
assert.NoError(t, f.SetSheetName("Sheet1", "sheet1"))
assert.Equal(t, "sheet1", f.GetSheetName(0))
// Test set sheet name with invalid sheet name // Test set sheet name with invalid sheet name
assert.EqualError(t, f.SetSheetName("Sheet:1", "Sheet1"), ErrSheetNameInvalid.Error()) assert.Equal(t, f.SetSheetName("Sheet:1", "Sheet1"), ErrSheetNameInvalid)
// Test set worksheet name with existing defined name and auto filter
assert.NoError(t, f.AutoFilter("Sheet1", "A1:A2", nil))
assert.NoError(t, f.SetDefinedName(&DefinedName{
Name: "Name1",
RefersTo: "$B$2",
}))
assert.NoError(t, f.SetDefinedName(&DefinedName{
Name: "Name2",
RefersTo: "$A1$2:A2",
}))
assert.NoError(t, f.SetDefinedName(&DefinedName{
Name: "Name3",
RefersTo: "Sheet1!$A$1:'Sheet1'!A1:Sheet1!$A$1,Sheet1!A1:Sheet3!A1,Sheet3!A1",
}))
assert.NoError(t, f.SetSheetName("Sheet1", "Sheet2"))
for i, expected := range []string{"'Sheet2'!$A$1:$A$2", "$B$2", "$A1$2:A2", "Sheet2!$A$1:'Sheet2'!A1:Sheet2!$A$1,Sheet2!A1:Sheet3!A1,Sheet3!A1"} {
assert.Equal(t, expected, f.WorkBook.DefinedNames.DefinedName[i].Data)
}
} }
func TestWorksheetWriter(t *testing.T) { func TestWorksheetWriter(t *testing.T) {
@ -428,7 +508,7 @@ func TestWorksheetWriter(t *testing.T) {
f.Sheet.Delete("xl/worksheets/sheet1.xml") f.Sheet.Delete("xl/worksheets/sheet1.xml")
worksheet := xml.Header + `<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><sheetData><row r="1"><c r="A1"><v>%d</v></c></row></sheetData><mc:AlternateContent xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"><mc:Choice xmlns:a14="http://schemas.microsoft.com/office/drawing/2010/main" Requires="a14"><xdr:twoCellAnchor editAs="oneCell"></xdr:twoCellAnchor></mc:Choice><mc:Fallback/></mc:AlternateContent></worksheet>` worksheet := xml.Header + `<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><sheetData><row r="1"><c r="A1"><v>%d</v></c></row></sheetData><mc:AlternateContent xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"><mc:Choice xmlns:a14="http://schemas.microsoft.com/office/drawing/2010/main" Requires="a14"><xdr:twoCellAnchor editAs="oneCell"></xdr:twoCellAnchor></mc:Choice><mc:Fallback/></mc:AlternateContent></worksheet>`
f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(worksheet, 1))) f.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(worksheet, 1)))
f.checked = nil f.checked = sync.Map{}
assert.NoError(t, f.SetCellValue("Sheet1", "A1", 2)) assert.NoError(t, f.SetCellValue("Sheet1", "A1", 2))
f.workSheetWriter() f.workSheetWriter()
value, ok := f.Pkg.Load("xl/worksheets/sheet1.xml") value, ok := f.Pkg.Load("xl/worksheets/sheet1.xml")
@ -478,6 +558,43 @@ func TestDeleteSheet(t *testing.T) {
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDeleteSheet2.xlsx"))) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDeleteSheet2.xlsx")))
} }
func TestMoveSheet(t *testing.T) {
f := NewFile()
defer f.Close()
for i := 2; i < 6; i++ {
_, err := f.NewSheet("Sheet" + strconv.Itoa(i))
assert.NoError(t, err)
}
assert.Equal(t, []string{"Sheet1", "Sheet2", "Sheet3", "Sheet4", "Sheet5"}, f.GetSheetList())
// Move target to first position
assert.NoError(t, f.MoveSheet("Sheet2", "Sheet1"))
assert.Equal(t, []string{"Sheet2", "Sheet1", "Sheet3", "Sheet4", "Sheet5"}, f.GetSheetList())
assert.Equal(t, "Sheet1", f.GetSheetName(f.GetActiveSheetIndex()))
// Move target to last position
assert.NoError(t, f.MoveSheet("Sheet2", "Sheet5"))
assert.NoError(t, f.MoveSheet("Sheet5", "Sheet2"))
assert.Equal(t, []string{"Sheet1", "Sheet3", "Sheet4", "Sheet5", "Sheet2"}, f.GetSheetList())
// Move target to same position
assert.NoError(t, f.MoveSheet("Sheet1", "Sheet1"))
assert.Equal(t, []string{"Sheet1", "Sheet3", "Sheet4", "Sheet5", "Sheet2"}, f.GetSheetList())
// Test move sheet with invalid sheet name
assert.Equal(t, ErrSheetNameBlank, f.MoveSheet("", "Sheet2"))
assert.Equal(t, ErrSheetNameBlank, f.MoveSheet("Sheet1", ""))
// Test move sheet on not exists worksheet
assert.Equal(t, ErrSheetNotExist{"SheetN"}, f.MoveSheet("SheetN", "Sheet2"))
assert.Equal(t, ErrSheetNotExist{"SheetN"}, f.MoveSheet("Sheet1", "SheetN"))
// Test move sheet with unsupported workbook charset
f.WorkBook = nil
f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset)
assert.EqualError(t, f.MoveSheet("Sheet2", "Sheet1"), "XML syntax error on line 1: invalid UTF-8")
}
func TestDeleteAndAdjustDefinedNames(t *testing.T) { func TestDeleteAndAdjustDefinedNames(t *testing.T) {
deleteAndAdjustDefinedNames(nil, 0) deleteAndAdjustDefinedNames(nil, 0)
deleteAndAdjustDefinedNames(&xlsxWorkbook{}, 0) deleteAndAdjustDefinedNames(&xlsxWorkbook{}, 0)
@ -501,6 +618,18 @@ func TestSetSheetVisible(t *testing.T) {
f.WorkBook = nil f.WorkBook = nil
f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset) f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset)
assert.EqualError(t, f.SetSheetVisible("Sheet1", false), "XML syntax error on line 1: invalid UTF-8") assert.EqualError(t, f.SetSheetVisible("Sheet1", false), "XML syntax error on line 1: invalid UTF-8")
// Test set sheet visible with empty sheet views
f = NewFile()
_, err := f.NewSheet("Sheet2")
assert.NoError(t, err)
ws, ok := f.Sheet.Load("xl/worksheets/sheet2.xml")
assert.True(t, ok)
ws.(*xlsxWorksheet).SheetViews = nil
assert.NoError(t, f.SetSheetVisible("Sheet2", false))
visible, err := f.GetSheetVisible("Sheet2")
assert.NoError(t, err)
assert.False(t, visible)
} }
func TestGetSheetVisible(t *testing.T) { func TestGetSheetVisible(t *testing.T) {
@ -527,12 +656,12 @@ func TestSetContentTypes(t *testing.T) {
assert.EqualError(t, f.setContentTypes("/xl/worksheets/sheet1.xml", ContentTypeSpreadSheetMLWorksheet), "XML syntax error on line 1: invalid UTF-8") assert.EqualError(t, f.setContentTypes("/xl/worksheets/sheet1.xml", ContentTypeSpreadSheetMLWorksheet), "XML syntax error on line 1: invalid UTF-8")
} }
func TestDeleteSheetFromContentTypes(t *testing.T) { func TestRemoveContentTypesPart(t *testing.T) {
f := NewFile() f := NewFile()
// Test delete sheet from content types with unsupported charset content types // Test delete sheet from content types with unsupported charset content types
f.ContentTypes = nil f.ContentTypes = nil
f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
assert.EqualError(t, f.deleteSheetFromContentTypes("/xl/worksheets/sheet1.xml"), "XML syntax error on line 1: invalid UTF-8") assert.EqualError(t, f.removeContentTypesPart(ContentTypeSpreadSheetMLWorksheet, "/xl/worksheets/sheet1.xml"), "XML syntax error on line 1: invalid UTF-8")
} }
func BenchmarkNewSheet(b *testing.B) { func BenchmarkNewSheet(b *testing.B) {

View File

@ -1,4 +1,4 @@
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in // this source code is governed by a BSD-style license that can be found in
// the LICENSE file. // the LICENSE file.
// //
@ -7,7 +7,7 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming // Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of // API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.16 or later. // data. This library needs Go version 1.18 or later.
package excelize package excelize
@ -116,7 +116,7 @@ func (ws *xlsxWorksheet) setSheetProps(opts *SheetPropsOptions) {
prepareTabColor := func(ws *xlsxWorksheet) { prepareTabColor := func(ws *xlsxWorksheet) {
ws.prepareSheetPr() ws.prepareSheetPr()
if ws.SheetPr.TabColor == nil { if ws.SheetPr.TabColor == nil {
ws.SheetPr.TabColor = new(xlsxTabColor) ws.SheetPr.TabColor = new(xlsxColor)
} }
} }
if opts.CodeName != nil { if opts.CodeName != nil {
@ -145,7 +145,12 @@ func (ws *xlsxWorksheet) setSheetProps(opts *SheetPropsOptions) {
if !s.Field(i).IsNil() { if !s.Field(i).IsNil() {
prepareTabColor(ws) prepareTabColor(ws)
name := s.Type().Field(i).Name name := s.Type().Field(i).Name
reflect.ValueOf(ws.SheetPr.TabColor).Elem().FieldByName(name[8:]).Set(s.Field(i).Elem()) fld := reflect.ValueOf(ws.SheetPr.TabColor).Elem().FieldByName(name[8:])
if s.Field(i).Kind() == reflect.Ptr && fld.Kind() == reflect.Ptr {
fld.Set(s.Field(i))
continue
}
fld.Set(s.Field(i).Elem())
} }
} }
} }
@ -206,7 +211,7 @@ func (f *File) GetSheetProps(sheet string) (SheetPropsOptions, error) {
if ws.SheetPr.TabColor != nil { if ws.SheetPr.TabColor != nil {
opts.TabColorIndexed = intPtr(ws.SheetPr.TabColor.Indexed) opts.TabColorIndexed = intPtr(ws.SheetPr.TabColor.Indexed)
opts.TabColorRGB = stringPtr(ws.SheetPr.TabColor.RGB) opts.TabColorRGB = stringPtr(ws.SheetPr.TabColor.RGB)
opts.TabColorTheme = intPtr(ws.SheetPr.TabColor.Theme) opts.TabColorTheme = ws.SheetPr.TabColor.Theme
opts.TabColorTint = float64Ptr(ws.SheetPr.TabColor.Tint) opts.TabColorTint = float64Ptr(ws.SheetPr.TabColor.Tint)
} }
} }

View File

@ -30,7 +30,7 @@ func TestSetPageMargins(t *testing.T) {
// Test set page margins on not exists worksheet // Test set page margins on not exists worksheet
assert.EqualError(t, f.SetPageMargins("SheetN", nil), "sheet SheetN does not exist") assert.EqualError(t, f.SetPageMargins("SheetN", nil), "sheet SheetN does not exist")
// Test set page margins with invalid sheet name // Test set page margins with invalid sheet name
assert.EqualError(t, f.SetPageMargins("Sheet:1", nil), ErrSheetNameInvalid.Error()) assert.Equal(t, ErrSheetNameInvalid, f.SetPageMargins("Sheet:1", nil))
} }
func TestGetPageMargins(t *testing.T) { func TestGetPageMargins(t *testing.T) {
@ -40,7 +40,7 @@ func TestGetPageMargins(t *testing.T) {
assert.EqualError(t, err, "sheet SheetN does not exist") assert.EqualError(t, err, "sheet SheetN does not exist")
// Test get page margins with invalid sheet name // Test get page margins with invalid sheet name
_, err = f.GetPageMargins("Sheet:1") _, err = f.GetPageMargins("Sheet:1")
assert.EqualError(t, err, ErrSheetNameInvalid.Error()) assert.Equal(t, ErrSheetNameInvalid, err)
} }
func TestSetSheetProps(t *testing.T) { func TestSetSheetProps(t *testing.T) {
@ -88,7 +88,7 @@ func TestSetSheetProps(t *testing.T) {
// Test set worksheet properties on not exists worksheet // Test set worksheet properties on not exists worksheet
assert.EqualError(t, f.SetSheetProps("SheetN", nil), "sheet SheetN does not exist") assert.EqualError(t, f.SetSheetProps("SheetN", nil), "sheet SheetN does not exist")
// Test set worksheet properties with invalid sheet name // Test set worksheet properties with invalid sheet name
assert.EqualError(t, f.SetSheetProps("Sheet:1", nil), ErrSheetNameInvalid.Error()) assert.Equal(t, ErrSheetNameInvalid, f.SetSheetProps("Sheet:1", nil))
} }
func TestGetSheetProps(t *testing.T) { func TestGetSheetProps(t *testing.T) {
@ -98,5 +98,5 @@ func TestGetSheetProps(t *testing.T) {
assert.EqualError(t, err, "sheet SheetN does not exist") assert.EqualError(t, err, "sheet SheetN does not exist")
// Test get worksheet properties with invalid sheet name // Test get worksheet properties with invalid sheet name
_, err = f.GetSheetProps("Sheet:1") _, err = f.GetSheetProps("Sheet:1")
assert.EqualError(t, err, ErrSheetNameInvalid.Error()) assert.Equal(t, ErrSheetNameInvalid, err)
} }

View File

@ -1,4 +1,4 @@
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in // this source code is governed by a BSD-style license that can be found in
// the LICENSE file. // the LICENSE file.
// //
@ -7,7 +7,7 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming // Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of // API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.16 or later. // data. This library needs Go version 1.18 or later.
package excelize package excelize
@ -61,11 +61,7 @@ func (view *xlsxSheetView) setSheetView(opts *ViewOptions) {
view.TopLeftCell = *opts.TopLeftCell view.TopLeftCell = *opts.TopLeftCell
} }
if opts.View != nil { if opts.View != nil {
if _, ok := map[string]interface{}{ if inStrSlice([]string{"normal", "pageLayout", "pageBreakPreview"}, *opts.View, true) != -1 {
"normal": nil,
"pageLayout": nil,
"pageBreakPreview": nil,
}[*opts.View]; ok {
view.View = *opts.View view.View = *opts.View
} }
} }

1050
slicer.go Normal file

File diff suppressed because it is too large Load Diff

621
slicer_test.go Normal file
View File

@ -0,0 +1,621 @@
package excelize
import (
"fmt"
"math/rand"
"os"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestSlicer(t *testing.T) {
f := NewFile()
disable, colName := false, "_!@#$%^&*()-+=|\\/<>"
assert.NoError(t, f.SetCellValue("Sheet1", "B1", colName))
// Create table in a worksheet
assert.NoError(t, f.AddTable("Sheet1", &Table{
Name: "Table1",
Range: "A1:D5",
}))
assert.NoError(t, f.AddSlicer("Sheet1", &SlicerOptions{
Name: "Column1",
Cell: "E1",
TableSheet: "Sheet1",
TableName: "Table1",
Caption: "Column1",
}))
assert.NoError(t, f.AddSlicer("Sheet1", &SlicerOptions{
Name: "Column1",
Cell: "I1",
TableSheet: "Sheet1",
TableName: "Table1",
Caption: "Column1",
}))
assert.NoError(t, f.AddSlicer("Sheet1", &SlicerOptions{
Name: colName,
Cell: "M1",
TableSheet: "Sheet1",
TableName: "Table1",
Caption: colName,
Macro: "Button1_Click",
Width: 200,
Height: 200,
DisplayHeader: &disable,
ItemDesc: true,
}))
// Test get table slicers
slicers, err := f.GetSlicers("Sheet1")
assert.NoError(t, err)
assert.Equal(t, "Column1", slicers[0].Name)
assert.Equal(t, "E1", slicers[0].Cell)
assert.Equal(t, "Sheet1", slicers[0].TableSheet)
assert.Equal(t, "Table1", slicers[0].TableName)
assert.Equal(t, "Column1", slicers[0].Caption)
assert.Equal(t, "Column1 1", slicers[1].Name)
assert.Equal(t, "I1", slicers[1].Cell)
assert.Equal(t, "Sheet1", slicers[1].TableSheet)
assert.Equal(t, "Table1", slicers[1].TableName)
assert.Equal(t, "Column1", slicers[1].Caption)
assert.Equal(t, colName, slicers[2].Name)
assert.Equal(t, "M1", slicers[2].Cell)
assert.Equal(t, "Sheet1", slicers[2].TableSheet)
assert.Equal(t, "Table1", slicers[2].TableName)
assert.Equal(t, colName, slicers[2].Caption)
assert.Equal(t, "Button1_Click", slicers[2].Macro)
assert.False(t, *slicers[2].DisplayHeader)
assert.True(t, slicers[2].ItemDesc)
// Test create two pivot tables in a new worksheet
_, err = f.NewSheet("Sheet2")
assert.NoError(t, err)
// Create some data in a sheet
month := []string{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}
year := []int{2017, 2018, 2019}
types := []string{"Meat", "Dairy", "Beverages", "Produce"}
region := []string{"East", "West", "North", "South"}
assert.NoError(t, f.SetSheetRow("Sheet2", "A1", &[]string{"Month", "Year", "Type", "Sales", "Region"}))
for row := 2; row < 32; row++ {
assert.NoError(t, f.SetCellValue("Sheet2", fmt.Sprintf("A%d", row), month[rand.Intn(12)]))
assert.NoError(t, f.SetCellValue("Sheet2", fmt.Sprintf("B%d", row), year[rand.Intn(3)]))
assert.NoError(t, f.SetCellValue("Sheet2", fmt.Sprintf("C%d", row), types[rand.Intn(4)]))
assert.NoError(t, f.SetCellValue("Sheet2", fmt.Sprintf("D%d", row), rand.Intn(5000)))
assert.NoError(t, f.SetCellValue("Sheet2", fmt.Sprintf("E%d", row), region[rand.Intn(4)]))
}
assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet2!A1:E31",
PivotTableRange: "Sheet2!G2:M34",
Name: "PivotTable1",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Filter: []PivotTableField{{Data: "Region"}},
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
Data: []PivotTableField{{Data: "Sales", Subtotal: "Sum", Name: "Summarize by Sum"}},
RowGrandTotals: true,
ColGrandTotals: true,
ShowDrill: true,
ShowRowHeaders: true,
ShowColHeaders: true,
ShowLastColumn: true,
ShowError: true,
PivotTableStyleName: "PivotStyleLight16",
}))
assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet2!A1:E31",
PivotTableRange: "Sheet2!U34:O2",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
Data: []PivotTableField{{Data: "Sales", Subtotal: "Average", Name: "Summarize by Average"}},
RowGrandTotals: true,
ColGrandTotals: true,
ShowDrill: true,
ShowRowHeaders: true,
ShowColHeaders: true,
ShowLastColumn: true,
}))
// Test add a pivot table slicer
assert.NoError(t, f.AddSlicer("Sheet2", &SlicerOptions{
Name: "Month",
Cell: "G42",
TableSheet: "Sheet2",
TableName: "PivotTable1",
Caption: "Month",
}))
// Test add a pivot table slicer with duplicate field name
assert.NoError(t, f.AddSlicer("Sheet2", &SlicerOptions{
Name: "Month",
Cell: "K42",
TableSheet: "Sheet2",
TableName: "PivotTable1",
Caption: "Month",
}))
// Test add a pivot table slicer for another pivot table in a worksheet
assert.NoError(t, f.AddSlicer("Sheet2", &SlicerOptions{
Name: "Region",
Cell: "O42",
TableSheet: "Sheet2",
TableName: "PivotTable2",
Caption: "Region",
ItemDesc: true,
}))
// Test get pivot table slicers
slicers, err = f.GetSlicers("Sheet2")
assert.NoError(t, err)
assert.Equal(t, "Month", slicers[0].Name)
assert.Equal(t, "G42", slicers[0].Cell)
assert.Equal(t, "Sheet2", slicers[0].TableSheet)
assert.Equal(t, "PivotTable1", slicers[0].TableName)
assert.Equal(t, "Month", slicers[0].Caption)
assert.Equal(t, "Month 1", slicers[1].Name)
assert.Equal(t, "K42", slicers[1].Cell)
assert.Equal(t, "Sheet2", slicers[1].TableSheet)
assert.Equal(t, "PivotTable1", slicers[1].TableName)
assert.Equal(t, "Month", slicers[1].Caption)
assert.Equal(t, "Region", slicers[2].Name)
assert.Equal(t, "O42", slicers[2].Cell)
assert.Equal(t, "Sheet2", slicers[2].TableSheet)
assert.Equal(t, "PivotTable2", slicers[2].TableName)
assert.Equal(t, "Region", slicers[2].Caption)
assert.True(t, slicers[2].ItemDesc)
// Test add a table slicer with empty slicer options
assert.Equal(t, ErrParameterRequired, f.AddSlicer("Sheet1", nil))
// Test add a table slicer with invalid slicer options
for _, opts := range []*SlicerOptions{
{Cell: "Q1", TableSheet: "Sheet1", TableName: "Table1"},
{Name: "Column", Cell: "Q1", TableSheet: "Sheet1"},
{Name: "Column", TableSheet: "Sheet1", TableName: "Table1"},
} {
assert.Equal(t, ErrParameterInvalid, f.AddSlicer("Sheet1", opts))
}
// Test add a table slicer with not exist worksheet
assert.EqualError(t, f.AddSlicer("SheetN", &SlicerOptions{
Name: "Column2",
Cell: "Q1",
TableSheet: "SheetN",
TableName: "Table1",
}), "sheet SheetN does not exist")
// Test add a table slicer with not exist table name
assert.Equal(t, newNoExistTableError("Table2"), f.AddSlicer("Sheet1", &SlicerOptions{
Name: "Column2",
Cell: "Q1",
TableSheet: "Sheet1",
TableName: "Table2",
}))
// Test add a table slicer with invalid slicer name
assert.Equal(t, newInvalidSlicerNameError("Column6"), f.AddSlicer("Sheet1", &SlicerOptions{
Name: "Column6",
Cell: "Q1",
TableSheet: "Sheet1",
TableName: "Table1",
}))
workbookPath := filepath.Join("test", "TestAddSlicer.xlsm")
file, err := os.ReadFile(filepath.Join("test", "vbaProject.bin"))
assert.NoError(t, err)
assert.NoError(t, f.AddVBAProject(file))
assert.NoError(t, f.SaveAs(workbookPath))
assert.NoError(t, f.Close())
// Test add a pivot table slicer with unsupported charset pivot table
f, err = OpenFile(workbookPath)
assert.NoError(t, err)
f.Pkg.Store("xl/pivotTables/pivotTable2.xml", MacintoshCyrillicCharset)
assert.EqualError(t, f.AddSlicer("Sheet2", &SlicerOptions{
Name: "Month",
Cell: "G42",
TableSheet: "Sheet2",
TableName: "PivotTable1",
Caption: "Month",
}), "XML syntax error on line 1: invalid UTF-8")
assert.NoError(t, f.Close())
// Test open a workbook and get already exist slicers
f, err = OpenFile(workbookPath)
assert.NoError(t, err)
slicers, err = f.GetSlicers("Sheet1")
assert.NoError(t, err)
assert.Equal(t, "Column1", slicers[0].Name)
assert.Equal(t, "E1", slicers[0].Cell)
assert.Equal(t, "Sheet1", slicers[0].TableSheet)
assert.Equal(t, "Table1", slicers[0].TableName)
assert.Equal(t, "Column1", slicers[0].Caption)
assert.Equal(t, "Column1 1", slicers[1].Name)
assert.Equal(t, "I1", slicers[1].Cell)
assert.Equal(t, "Sheet1", slicers[1].TableSheet)
assert.Equal(t, "Table1", slicers[1].TableName)
assert.Equal(t, "Column1", slicers[1].Caption)
assert.Equal(t, colName, slicers[2].Name)
assert.Equal(t, "M1", slicers[2].Cell)
assert.Equal(t, "Sheet1", slicers[2].TableSheet)
assert.Equal(t, "Table1", slicers[2].TableName)
assert.Equal(t, colName, slicers[2].Caption)
assert.Equal(t, "Button1_Click", slicers[2].Macro)
assert.False(t, *slicers[2].DisplayHeader)
assert.True(t, slicers[2].ItemDesc)
slicers, err = f.GetSlicers("Sheet2")
assert.NoError(t, err)
assert.Equal(t, "Month", slicers[0].Name)
assert.Equal(t, "G42", slicers[0].Cell)
assert.Equal(t, "Sheet2", slicers[0].TableSheet)
assert.Equal(t, "PivotTable1", slicers[0].TableName)
assert.Equal(t, "Month", slicers[0].Caption)
assert.Equal(t, "Month 1", slicers[1].Name)
assert.Equal(t, "K42", slicers[1].Cell)
assert.Equal(t, "Sheet2", slicers[1].TableSheet)
assert.Equal(t, "PivotTable1", slicers[1].TableName)
assert.Equal(t, "Month", slicers[1].Caption)
assert.Equal(t, "Region", slicers[2].Name)
assert.Equal(t, "O42", slicers[2].Cell)
assert.Equal(t, "Sheet2", slicers[2].TableSheet)
assert.Equal(t, "PivotTable2", slicers[2].TableName)
assert.Equal(t, "Region", slicers[2].Caption)
assert.True(t, slicers[2].ItemDesc)
// Test add a pivot table slicer with workbook which contains timeline
f, err = OpenFile(workbookPath)
assert.NoError(t, err)
f.Pkg.Store("xl/timelines/timeline1.xml", []byte(fmt.Sprintf(`<timelines xmlns="%s"><timeline name="a"/></timelines>`, NameSpaceSpreadSheetX15.Value)))
assert.NoError(t, f.AddSlicer("Sheet2", &SlicerOptions{
Name: "Month",
Cell: "G42",
TableSheet: "Sheet2",
TableName: "PivotTable1",
Caption: "Month",
}))
assert.NoError(t, f.Close())
// Test add a pivot table slicer with unsupported charset timeline
f, err = OpenFile(workbookPath)
assert.NoError(t, err)
f.Pkg.Store("xl/timelines/timeline1.xml", MacintoshCyrillicCharset)
assert.NoError(t, f.AddSlicer("Sheet2", &SlicerOptions{
Name: "Month",
Cell: "G42",
TableSheet: "Sheet2",
TableName: "PivotTable1",
Caption: "Month",
}))
assert.NoError(t, f.Close())
// Test add a table slicer with invalid worksheet extension list
f = NewFile()
assert.NoError(t, f.AddTable("Sheet1", &Table{
Name: "Table1",
Range: "A1:D5",
}))
ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
assert.True(t, ok)
ws.(*xlsxWorksheet).ExtLst = &xlsxExtLst{Ext: "<>"}
assert.Error(t, f.AddSlicer("Sheet1", &SlicerOptions{
Name: "Column1",
Cell: "E1",
TableSheet: "Sheet1",
TableName: "Table1",
}))
assert.NoError(t, f.Close())
// Test add a table slicer with unsupported charset slicer
f = NewFile()
assert.NoError(t, f.AddTable("Sheet1", &Table{
Name: "Table1",
Range: "A1:D5",
}))
f.Pkg.Store("xl/slicers/slicer2.xml", MacintoshCyrillicCharset)
assert.EqualError(t, f.AddSlicer("Sheet1", &SlicerOptions{
Name: "Column1",
Cell: "E1",
TableName: "Table1",
TableSheet: "Sheet1",
}), "XML syntax error on line 1: invalid UTF-8")
assert.NoError(t, f.Close())
// Test add a table slicer with read workbook error
f = NewFile()
assert.NoError(t, f.AddTable("Sheet1", &Table{
Name: "Table1",
Range: "A1:D5",
}))
f.WorkBook.ExtLst = &xlsxExtLst{Ext: "<>"}
assert.Error(t, f.AddSlicer("Sheet1", &SlicerOptions{
Name: "Column1",
Cell: "E1",
TableName: "Table1",
TableSheet: "Sheet1",
}))
assert.NoError(t, f.Close())
// Test add a table slicer with unsupported charset content types
f = NewFile()
assert.NoError(t, f.AddTable("Sheet1", &Table{
Name: "Table1",
Range: "A1:D5",
}))
f.ContentTypes = nil
f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
assert.EqualError(t, f.AddSlicer("Sheet1", &SlicerOptions{
Name: "Column1",
Cell: "E1",
TableName: "Table1",
TableSheet: "Sheet1",
}), "XML syntax error on line 1: invalid UTF-8")
f.ContentTypes = nil
f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
assert.EqualError(t, f.addSlicer(0, xlsxSlicer{}), "XML syntax error on line 1: invalid UTF-8")
assert.NoError(t, f.Close())
f = NewFile()
// Create table in a worksheet
assert.NoError(t, f.AddTable("Sheet1", &Table{
Name: "Table1",
Range: "A1:D5",
}))
f.Pkg.Store("xl/drawings/drawing2.xml", MacintoshCyrillicCharset)
assert.EqualError(t, f.AddSlicer("Sheet1", &SlicerOptions{
Name: "Column1",
Cell: "E1",
TableSheet: "Sheet1",
TableName: "Table1",
Caption: "Column1",
}), "XML syntax error on line 1: invalid UTF-8")
assert.NoError(t, f.Close())
f = NewFile()
// Test get sheet slicers without slicer
slicers, err = f.GetSlicers("Sheet1")
assert.NoError(t, err)
assert.Empty(t, slicers)
// Test get sheet slicers with not exist worksheet name
_, err = f.GetSlicers("SheetN")
assert.EqualError(t, err, "sheet SheetN does not exist")
assert.NoError(t, f.Close())
f, err = OpenFile(workbookPath)
assert.NoError(t, err)
// Test get sheet slicers with unsupported charset slicer cache
f.Pkg.Store("xl/slicerCaches/slicerCache1.xml", MacintoshCyrillicCharset)
_, err = f.GetSlicers("Sheet1")
assert.NoError(t, err)
// Test get sheet slicers with unsupported charset slicer
f.Pkg.Store("xl/slicers/slicer1.xml", MacintoshCyrillicCharset)
_, err = f.GetSlicers("Sheet1")
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
// Test get sheet slicers with invalid worksheet extension list
ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml")
assert.True(t, ok)
ws.(*xlsxWorksheet).ExtLst.Ext = "<>"
_, err = f.GetSlicers("Sheet1")
assert.Error(t, err)
assert.NoError(t, f.Close())
f, err = OpenFile(workbookPath)
assert.NoError(t, err)
// Test get sheet slicers without slicer cache
f.Pkg.Range(func(k, v interface{}) bool {
if strings.Contains(k.(string), "xl/slicerCaches/slicerCache") {
f.Pkg.Delete(k.(string))
}
return true
})
slicers, err = f.GetSlicers("Sheet1")
assert.NoError(t, err)
assert.Empty(t, slicers)
assert.NoError(t, f.Close())
// Test open a workbook and get sheet slicer with invalid cell reference in the drawing part
f, err = OpenFile(workbookPath)
assert.NoError(t, err)
f.Pkg.Store("xl/drawings/drawing1.xml", []byte(fmt.Sprintf(`<wsDr xmlns="%s"><twoCellAnchor><from><col>-1</col><row>-1</row></from><mc:AlternateContent><mc:Choice xmlns:sle15="%s"><graphicFrame><nvGraphicFramePr><cNvPr id="2" name="Column1"/></nvGraphicFramePr></graphicFrame></mc:Choice></mc:AlternateContent></twoCellAnchor></wsDr>`, NameSpaceDrawingMLSpreadSheet.Value, NameSpaceDrawingMLSlicerX15.Value)))
_, err = f.GetSlicers("Sheet1")
assert.Equal(t, newCoordinatesToCellNameError(0, 0), err)
// Test get sheet slicer without slicer shape in the drawing part
f.Drawings.Delete("xl/drawings/drawing1.xml")
f.Pkg.Store("xl/drawings/drawing1.xml", []byte(fmt.Sprintf(`<wsDr xmlns="%s"><twoCellAnchor/></wsDr>`, NameSpaceDrawingMLSpreadSheet.Value)))
_, err = f.GetSlicers("Sheet1")
assert.NoError(t, err)
f.Drawings.Delete("xl/drawings/drawing1.xml")
// Test get sheet slicers with unsupported charset drawing part
f.Pkg.Store("xl/drawings/drawing1.xml", MacintoshCyrillicCharset)
_, err = f.GetSlicers("Sheet1")
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
// Test get sheet slicers with unsupported charset table
f.Pkg.Store("xl/tables/table1.xml", MacintoshCyrillicCharset)
_, err = f.GetSlicers("Sheet1")
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
// Test get sheet slicers with unsupported charset pivot table
f.Pkg.Store("xl/pivotTables/pivotTable1.xml", MacintoshCyrillicCharset)
_, err = f.GetSlicers("Sheet2")
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
assert.NoError(t, f.Close())
// Test create a workbook and get sheet slicer with invalid cell reference in the drawing part
f = NewFile()
assert.NoError(t, f.AddTable("Sheet1", &Table{
Name: "Table1",
Range: "A1:D5",
}))
assert.NoError(t, f.AddSlicer("Sheet1", &SlicerOptions{
Name: "Column1",
Cell: "E1",
TableSheet: "Sheet1",
TableName: "Table1",
Caption: "Column1",
}))
drawing, ok := f.Drawings.Load("xl/drawings/drawing1.xml")
assert.True(t, ok)
drawing.(*xlsxWsDr).TwoCellAnchor[0].From = &xlsxFrom{Col: -1, Row: -1}
_, err = f.GetSlicers("Sheet1")
assert.Equal(t, newCoordinatesToCellNameError(0, 0), err)
assert.NoError(t, f.Close())
// Test open a workbook and delete slicers
f, err = OpenFile(workbookPath)
assert.NoError(t, err)
for _, name := range []string{colName, "Column1 1", "Column1"} {
assert.NoError(t, f.DeleteSlicer(name))
}
for _, name := range []string{"Month", "Month 1", "Region"} {
assert.NoError(t, f.DeleteSlicer(name))
}
// Test delete slicer with no exits slicer name
assert.Equal(t, newNoExistSlicerError("x"), f.DeleteSlicer("x"))
assert.NoError(t, f.Close())
// Test open a workbook and delete sheet slicer with unsupported charset slicer cache
f, err = OpenFile(workbookPath)
assert.NoError(t, err)
f.Pkg.Store("xl/slicers/slicer1.xml", MacintoshCyrillicCharset)
assert.EqualError(t, f.DeleteSlicer("Column1"), "XML syntax error on line 1: invalid UTF-8")
assert.NoError(t, f.Close())
}
func TestAddSheetSlicer(t *testing.T) {
f := NewFile()
// Test add sheet slicer with not exist worksheet name
_, err := f.addSheetSlicer("SheetN", ExtURISlicerListX15)
assert.EqualError(t, err, "sheet SheetN does not exist")
assert.NoError(t, f.Close())
}
func TestAddSheetTableSlicer(t *testing.T) {
f := NewFile()
// Test add sheet table slicer with invalid worksheet extension
assert.Error(t, f.addSheetTableSlicer(&xlsxWorksheet{ExtLst: &xlsxExtLst{Ext: "<>"}}, 0, ExtURISlicerListX15))
// Test add sheet table slicer with existing worksheet extension
assert.NoError(t, f.addSheetTableSlicer(&xlsxWorksheet{ExtLst: &xlsxExtLst{Ext: fmt.Sprintf("<ext uri=\"%s\"></ext>", ExtURITimelineRefs)}}, 1, ExtURISlicerListX15))
assert.NoError(t, f.Close())
}
func TestSetSlicerCache(t *testing.T) {
f := NewFile()
f.Pkg.Store("xl/slicerCaches/slicerCache1.xml", MacintoshCyrillicCharset)
_, err := f.setSlicerCache(1, &SlicerOptions{}, &Table{}, nil)
assert.NoError(t, err)
assert.NoError(t, f.Close())
f = NewFile()
f.Pkg.Store("xl/slicerCaches/slicerCache2.xml", []byte(fmt.Sprintf(`<slicerCacheDefinition xmlns="%s" name="Slicer2" sourceName="B1"><extLst><ext uri="%s"/></extLst></slicerCacheDefinition>`, NameSpaceSpreadSheetX14.Value, ExtURISlicerCacheDefinition)))
_, err = f.setSlicerCache(1, &SlicerOptions{}, &Table{}, nil)
assert.NoError(t, err)
assert.NoError(t, f.Close())
f = NewFile()
f.Pkg.Store("xl/slicerCaches/slicerCache2.xml", []byte(fmt.Sprintf(`<slicerCacheDefinition xmlns="%s" name="Slicer1" sourceName="B1"><extLst><ext uri="%s"/></extLst></slicerCacheDefinition>`, NameSpaceSpreadSheetX14.Value, ExtURISlicerCacheDefinition)))
_, err = f.setSlicerCache(1, &SlicerOptions{}, &Table{}, nil)
assert.NoError(t, err)
assert.NoError(t, f.Close())
f = NewFile()
f.Pkg.Store("xl/slicerCaches/slicerCache2.xml", []byte(fmt.Sprintf(`<slicerCacheDefinition xmlns="%s" name="Slicer1" sourceName="B1"><extLst><ext uri="%s"><tableSlicerCache tableId="1" column="2"/></ext></extLst></slicerCacheDefinition>`, NameSpaceSpreadSheetX14.Value, ExtURISlicerCacheDefinition)))
_, err = f.setSlicerCache(1, &SlicerOptions{}, &Table{tID: 1}, nil)
assert.NoError(t, err)
assert.NoError(t, f.Close())
f = NewFile()
f.Pkg.Store("xl/slicerCaches/slicerCache2.xml", []byte(fmt.Sprintf(`<slicerCacheDefinition xmlns="%s" name="Slicer1" sourceName="B1"></slicerCacheDefinition>`, NameSpaceSpreadSheetX14.Value)))
_, err = f.setSlicerCache(1, &SlicerOptions{}, &Table{tID: 1}, nil)
assert.NoError(t, err)
assert.NoError(t, f.Close())
}
func TestDeleteSlicer(t *testing.T) {
f, slicerXML := NewFile(), "xl/slicers/slicer1.xml"
assert.NoError(t, f.AddTable("Sheet1", &Table{
Name: "Table1",
Range: "A1:D5",
}))
assert.NoError(t, f.AddSlicer("Sheet1", &SlicerOptions{
Name: "Column1",
Cell: "E1",
TableSheet: "Sheet1",
TableName: "Table1",
Caption: "Column1",
}))
// Test delete sheet slicers with invalid worksheet extension list
ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
assert.True(t, ok)
ws.(*xlsxWorksheet).ExtLst.Ext = "<>"
assert.Error(t, f.deleteSlicer(SlicerOptions{
slicerXML: slicerXML,
slicerSheetName: "Sheet1",
Name: "Column1",
}))
// Test delete slicer with unsupported charset worksheet
f.Sheet.Delete("xl/worksheets/sheet1.xml")
f.Pkg.Store("xl/worksheets/sheet1.xml", MacintoshCyrillicCharset)
assert.EqualError(t, f.deleteSlicer(SlicerOptions{
slicerXML: slicerXML,
slicerSheetName: "Sheet1",
Name: "Column1",
}), "XML syntax error on line 1: invalid UTF-8")
// Test delete slicer with unsupported charset slicer
f.Pkg.Store(slicerXML, MacintoshCyrillicCharset)
assert.EqualError(t, f.deleteSlicer(SlicerOptions{slicerXML: slicerXML}), "XML syntax error on line 1: invalid UTF-8")
assert.NoError(t, f.Close())
}
func TestDeleteSlicerCache(t *testing.T) {
f := NewFile()
// Test delete slicer cache with unsupported charset workbook
f.WorkBook = nil
f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset)
assert.EqualError(t, f.deleteSlicerCache(nil, SlicerOptions{}), "XML syntax error on line 1: invalid UTF-8")
assert.NoError(t, f.Close())
}
func TestAddSlicerCache(t *testing.T) {
f := NewFile()
f.ContentTypes = nil
f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
assert.EqualError(t, f.addSlicerCache("Slicer1", 0, &SlicerOptions{}, &Table{}, nil), "XML syntax error on line 1: invalid UTF-8")
// Test add a pivot table cache slicer with unsupported charset
pivotCacheXML := "xl/pivotCache/pivotCacheDefinition1.xml"
f.Pkg.Store(pivotCacheXML, MacintoshCyrillicCharset)
assert.EqualError(t, f.addSlicerCache("Slicer1", 0, &SlicerOptions{}, nil,
&PivotTableOptions{pivotCacheXML: pivotCacheXML}), "XML syntax error on line 1: invalid UTF-8")
assert.NoError(t, f.Close())
}
func TestAddDrawingSlicer(t *testing.T) {
f := NewFile()
// Test add a drawing slicer with not exist worksheet
assert.EqualError(t, f.addDrawingSlicer("SheetN", "Column2", NameSpaceDrawingMLSlicerX15, &SlicerOptions{
Name: "Column2",
Cell: "Q1",
TableSheet: "SheetN",
TableName: "Table1",
}), "sheet SheetN does not exist")
// Test add a drawing slicer with invalid cell reference
assert.EqualError(t, f.addDrawingSlicer("Sheet1", "Column2", NameSpaceDrawingMLSlicerX15, &SlicerOptions{
Name: "Column2",
Cell: "A",
TableSheet: "Sheet1",
TableName: "Table1",
}), "cannot convert cell \"A\" to coordinates: invalid cell name \"A\"")
assert.NoError(t, f.Close())
}
func TestAddWorkbookSlicerCache(t *testing.T) {
// Test add a workbook slicer cache with unsupported charset workbook
f := NewFile()
f.WorkBook = nil
f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset)
assert.EqualError(t, f.addWorkbookSlicerCache(1, ExtURISlicerCachesX15), "XML syntax error on line 1: invalid UTF-8")
assert.NoError(t, f.Close())
}
func TestGenSlicerCacheName(t *testing.T) {
f := NewFile()
assert.NoError(t, f.SetDefinedName(&DefinedName{Name: "Slicer_Column_1", RefersTo: formulaErrorNA}))
assert.Equal(t, "Slicer_Column_11", f.genSlicerCacheName("Column 1"))
assert.NoError(t, f.Close())
}
func TestAddPivotCacheSlicer(t *testing.T) {
f := NewFile()
pivotCacheXML := "xl/pivotCache/pivotCacheDefinition1.xml"
// Test add a pivot table cache slicer with existing extension list
f.Pkg.Store(pivotCacheXML, []byte(fmt.Sprintf(`<pivotCacheDefinition xmlns="%s"><extLst><ext uri="%s"><x14:pivotCacheDefinition pivotCacheId="1"/></ext></extLst></pivotCacheDefinition>`, NameSpaceSpreadSheet.Value, ExtURIPivotCacheDefinition)))
_, err := f.addPivotCacheSlicer(&PivotTableOptions{
pivotCacheXML: pivotCacheXML,
})
assert.NoError(t, err)
}

View File

@ -1,4 +1,4 @@
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in // this source code is governed by a BSD-style license that can be found in
// the LICENSE file. // the LICENSE file.
// //
@ -7,7 +7,7 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming // Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of // API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.16 or later. // data. This library needs Go version 1.18 or later.
package excelize package excelize
@ -18,353 +18,352 @@ import (
"strings" "strings"
) )
// addSparklineGroupByStyle provides a function to create x14:sparklineGroups // getSparklineGroupPresets returns the preset list of sparkline group to create
// element by given sparkline style ID. // x14:sparklineGroups element.
func (f *File) addSparklineGroupByStyle(ID int) *xlsxX14SparklineGroup { func getSparklineGroupPresets() []*xlsxX14SparklineGroup {
groups := []*xlsxX14SparklineGroup{ return []*xlsxX14SparklineGroup{
{ {
ColorSeries: &xlsxTabColor{Theme: 4, Tint: -0.499984740745262}, ColorSeries: &xlsxColor{Theme: intPtr(4), Tint: -0.499984740745262},
ColorNegative: &xlsxTabColor{Theme: 5}, ColorNegative: &xlsxColor{Theme: intPtr(5)},
ColorMarkers: &xlsxTabColor{Theme: 4, Tint: -0.499984740745262}, ColorMarkers: &xlsxColor{Theme: intPtr(4), Tint: -0.499984740745262},
ColorFirst: &xlsxTabColor{Theme: 4, Tint: 0.39997558519241921}, ColorFirst: &xlsxColor{Theme: intPtr(4), Tint: 0.39997558519241921},
ColorLast: &xlsxTabColor{Theme: 4, Tint: 0.39997558519241921}, ColorLast: &xlsxColor{Theme: intPtr(4), Tint: 0.39997558519241921},
ColorHigh: &xlsxTabColor{Theme: 4}, ColorHigh: &xlsxColor{Theme: intPtr(4)},
ColorLow: &xlsxTabColor{Theme: 4}, ColorLow: &xlsxColor{Theme: intPtr(4)},
}, // 0 }, // 0
{ {
ColorSeries: &xlsxTabColor{Theme: 4, Tint: -0.499984740745262}, ColorSeries: &xlsxColor{Theme: intPtr(4), Tint: -0.499984740745262},
ColorNegative: &xlsxTabColor{Theme: 5}, ColorNegative: &xlsxColor{Theme: intPtr(5)},
ColorMarkers: &xlsxTabColor{Theme: 4, Tint: -0.499984740745262}, ColorMarkers: &xlsxColor{Theme: intPtr(4), Tint: -0.499984740745262},
ColorFirst: &xlsxTabColor{Theme: 4, Tint: 0.39997558519241921}, ColorFirst: &xlsxColor{Theme: intPtr(4), Tint: 0.39997558519241921},
ColorLast: &xlsxTabColor{Theme: 4, Tint: 0.39997558519241921}, ColorLast: &xlsxColor{Theme: intPtr(4), Tint: 0.39997558519241921},
ColorHigh: &xlsxTabColor{Theme: 4}, ColorHigh: &xlsxColor{Theme: intPtr(4)},
ColorLow: &xlsxTabColor{Theme: 4}, ColorLow: &xlsxColor{Theme: intPtr(4)},
}, // 1 }, // 1
{ {
ColorSeries: &xlsxTabColor{Theme: 5, Tint: -0.499984740745262}, ColorSeries: &xlsxColor{Theme: intPtr(5), Tint: -0.499984740745262},
ColorNegative: &xlsxTabColor{Theme: 6}, ColorNegative: &xlsxColor{Theme: intPtr(6)},
ColorMarkers: &xlsxTabColor{Theme: 5, Tint: -0.499984740745262}, ColorMarkers: &xlsxColor{Theme: intPtr(5), Tint: -0.499984740745262},
ColorFirst: &xlsxTabColor{Theme: 5, Tint: 0.39997558519241921}, ColorFirst: &xlsxColor{Theme: intPtr(5), Tint: 0.39997558519241921},
ColorLast: &xlsxTabColor{Theme: 5, Tint: 0.39997558519241921}, ColorLast: &xlsxColor{Theme: intPtr(5), Tint: 0.39997558519241921},
ColorHigh: &xlsxTabColor{Theme: 5}, ColorHigh: &xlsxColor{Theme: intPtr(5)},
ColorLow: &xlsxTabColor{Theme: 5}, ColorLow: &xlsxColor{Theme: intPtr(5)},
}, // 2 }, // 2
{ {
ColorSeries: &xlsxTabColor{Theme: 6, Tint: -0.499984740745262}, ColorSeries: &xlsxColor{Theme: intPtr(6), Tint: -0.499984740745262},
ColorNegative: &xlsxTabColor{Theme: 7}, ColorNegative: &xlsxColor{Theme: intPtr(7)},
ColorMarkers: &xlsxTabColor{Theme: 6, Tint: -0.499984740745262}, ColorMarkers: &xlsxColor{Theme: intPtr(6), Tint: -0.499984740745262},
ColorFirst: &xlsxTabColor{Theme: 6, Tint: 0.39997558519241921}, ColorFirst: &xlsxColor{Theme: intPtr(6), Tint: 0.39997558519241921},
ColorLast: &xlsxTabColor{Theme: 6, Tint: 0.39997558519241921}, ColorLast: &xlsxColor{Theme: intPtr(6), Tint: 0.39997558519241921},
ColorHigh: &xlsxTabColor{Theme: 6}, ColorHigh: &xlsxColor{Theme: intPtr(6)},
ColorLow: &xlsxTabColor{Theme: 6}, ColorLow: &xlsxColor{Theme: intPtr(6)},
}, // 3 }, // 3
{ {
ColorSeries: &xlsxTabColor{Theme: 7, Tint: -0.499984740745262}, ColorSeries: &xlsxColor{Theme: intPtr(7), Tint: -0.499984740745262},
ColorNegative: &xlsxTabColor{Theme: 8}, ColorNegative: &xlsxColor{Theme: intPtr(8)},
ColorMarkers: &xlsxTabColor{Theme: 7, Tint: -0.499984740745262}, ColorMarkers: &xlsxColor{Theme: intPtr(7), Tint: -0.499984740745262},
ColorFirst: &xlsxTabColor{Theme: 7, Tint: 0.39997558519241921}, ColorFirst: &xlsxColor{Theme: intPtr(7), Tint: 0.39997558519241921},
ColorLast: &xlsxTabColor{Theme: 7, Tint: 0.39997558519241921}, ColorLast: &xlsxColor{Theme: intPtr(7), Tint: 0.39997558519241921},
ColorHigh: &xlsxTabColor{Theme: 7}, ColorHigh: &xlsxColor{Theme: intPtr(7)},
ColorLow: &xlsxTabColor{Theme: 7}, ColorLow: &xlsxColor{Theme: intPtr(7)},
}, // 4 }, // 4
{ {
ColorSeries: &xlsxTabColor{Theme: 8, Tint: -0.499984740745262}, ColorSeries: &xlsxColor{Theme: intPtr(8), Tint: -0.499984740745262},
ColorNegative: &xlsxTabColor{Theme: 9}, ColorNegative: &xlsxColor{Theme: intPtr(9)},
ColorMarkers: &xlsxTabColor{Theme: 8, Tint: -0.499984740745262}, ColorMarkers: &xlsxColor{Theme: intPtr(8), Tint: -0.499984740745262},
ColorFirst: &xlsxTabColor{Theme: 8, Tint: 0.39997558519241921}, ColorFirst: &xlsxColor{Theme: intPtr(8), Tint: 0.39997558519241921},
ColorLast: &xlsxTabColor{Theme: 8, Tint: 0.39997558519241921}, ColorLast: &xlsxColor{Theme: intPtr(8), Tint: 0.39997558519241921},
ColorHigh: &xlsxTabColor{Theme: 8}, ColorHigh: &xlsxColor{Theme: intPtr(8)},
ColorLow: &xlsxTabColor{Theme: 8}, ColorLow: &xlsxColor{Theme: intPtr(8)},
}, // 5 }, // 5
{ {
ColorSeries: &xlsxTabColor{Theme: 9, Tint: -0.499984740745262}, ColorSeries: &xlsxColor{Theme: intPtr(9), Tint: -0.499984740745262},
ColorNegative: &xlsxTabColor{Theme: 4}, ColorNegative: &xlsxColor{Theme: intPtr(4)},
ColorMarkers: &xlsxTabColor{Theme: 9, Tint: -0.499984740745262}, ColorMarkers: &xlsxColor{Theme: intPtr(9), Tint: -0.499984740745262},
ColorFirst: &xlsxTabColor{Theme: 9, Tint: 0.39997558519241921}, ColorFirst: &xlsxColor{Theme: intPtr(9), Tint: 0.39997558519241921},
ColorLast: &xlsxTabColor{Theme: 9, Tint: 0.39997558519241921}, ColorLast: &xlsxColor{Theme: intPtr(9), Tint: 0.39997558519241921},
ColorHigh: &xlsxTabColor{Theme: 9}, ColorHigh: &xlsxColor{Theme: intPtr(9)},
ColorLow: &xlsxTabColor{Theme: 9}, ColorLow: &xlsxColor{Theme: intPtr(9)},
}, // 6 }, // 6
{ {
ColorSeries: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893}, ColorSeries: &xlsxColor{Theme: intPtr(4), Tint: -0.249977111117893},
ColorNegative: &xlsxTabColor{Theme: 5}, ColorNegative: &xlsxColor{Theme: intPtr(5)},
ColorMarkers: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893}, ColorMarkers: &xlsxColor{Theme: intPtr(5), Tint: -0.249977111117893},
ColorFirst: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893}, ColorFirst: &xlsxColor{Theme: intPtr(5), Tint: -0.249977111117893},
ColorLast: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893}, ColorLast: &xlsxColor{Theme: intPtr(5), Tint: -0.249977111117893},
ColorHigh: &xlsxTabColor{Theme: 5}, ColorHigh: &xlsxColor{Theme: intPtr(5)},
ColorLow: &xlsxTabColor{Theme: 5}, ColorLow: &xlsxColor{Theme: intPtr(5)},
}, // 7 }, // 7
{ {
ColorSeries: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893}, ColorSeries: &xlsxColor{Theme: intPtr(5), Tint: -0.249977111117893},
ColorNegative: &xlsxTabColor{Theme: 6}, ColorNegative: &xlsxColor{Theme: intPtr(6)},
ColorMarkers: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893}, ColorMarkers: &xlsxColor{Theme: intPtr(6), Tint: -0.249977111117893},
ColorFirst: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893}, ColorFirst: &xlsxColor{Theme: intPtr(6), Tint: -0.249977111117893},
ColorLast: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893}, ColorLast: &xlsxColor{Theme: intPtr(6), Tint: -0.249977111117893},
ColorHigh: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893}, ColorHigh: &xlsxColor{Theme: intPtr(6), Tint: -0.249977111117893},
ColorLow: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893}, ColorLow: &xlsxColor{Theme: intPtr(6), Tint: -0.249977111117893},
}, // 8 }, // 8
{ {
ColorSeries: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893}, ColorSeries: &xlsxColor{Theme: intPtr(6), Tint: -0.249977111117893},
ColorNegative: &xlsxTabColor{Theme: 7}, ColorNegative: &xlsxColor{Theme: intPtr(7)},
ColorMarkers: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893}, ColorMarkers: &xlsxColor{Theme: intPtr(7), Tint: -0.249977111117893},
ColorFirst: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893}, ColorFirst: &xlsxColor{Theme: intPtr(7), Tint: -0.249977111117893},
ColorLast: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893}, ColorLast: &xlsxColor{Theme: intPtr(7), Tint: -0.249977111117893},
ColorHigh: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893}, ColorHigh: &xlsxColor{Theme: intPtr(7), Tint: -0.249977111117893},
ColorLow: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893}, ColorLow: &xlsxColor{Theme: intPtr(7), Tint: -0.249977111117893},
}, // 9 }, // 9
{ {
ColorSeries: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893}, ColorSeries: &xlsxColor{Theme: intPtr(7), Tint: -0.249977111117893},
ColorNegative: &xlsxTabColor{Theme: 8}, ColorNegative: &xlsxColor{Theme: intPtr(8)},
ColorMarkers: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893}, ColorMarkers: &xlsxColor{Theme: intPtr(8), Tint: -0.249977111117893},
ColorFirst: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893}, ColorFirst: &xlsxColor{Theme: intPtr(8), Tint: -0.249977111117893},
ColorLast: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893}, ColorLast: &xlsxColor{Theme: intPtr(8), Tint: -0.249977111117893},
ColorHigh: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893}, ColorHigh: &xlsxColor{Theme: intPtr(8), Tint: -0.249977111117893},
ColorLow: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893}, ColorLow: &xlsxColor{Theme: intPtr(8), Tint: -0.249977111117893},
}, // 10 }, // 10
{ {
ColorSeries: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893}, ColorSeries: &xlsxColor{Theme: intPtr(8), Tint: -0.249977111117893},
ColorNegative: &xlsxTabColor{Theme: 9}, ColorNegative: &xlsxColor{Theme: intPtr(9)},
ColorMarkers: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893}, ColorMarkers: &xlsxColor{Theme: intPtr(9), Tint: -0.249977111117893},
ColorFirst: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893}, ColorFirst: &xlsxColor{Theme: intPtr(9), Tint: -0.249977111117893},
ColorLast: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893}, ColorLast: &xlsxColor{Theme: intPtr(9), Tint: -0.249977111117893},
ColorHigh: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893}, ColorHigh: &xlsxColor{Theme: intPtr(9), Tint: -0.249977111117893},
ColorLow: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893}, ColorLow: &xlsxColor{Theme: intPtr(9), Tint: -0.249977111117893},
}, // 11 }, // 11
{ {
ColorSeries: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893}, ColorSeries: &xlsxColor{Theme: intPtr(9), Tint: -0.249977111117893},
ColorNegative: &xlsxTabColor{Theme: 4}, ColorNegative: &xlsxColor{Theme: intPtr(4)},
ColorMarkers: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893}, ColorMarkers: &xlsxColor{Theme: intPtr(4), Tint: -0.249977111117893},
ColorFirst: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893}, ColorFirst: &xlsxColor{Theme: intPtr(4), Tint: -0.249977111117893},
ColorLast: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893}, ColorLast: &xlsxColor{Theme: intPtr(4), Tint: -0.249977111117893},
ColorHigh: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893}, ColorHigh: &xlsxColor{Theme: intPtr(4), Tint: -0.249977111117893},
ColorLow: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893}, ColorLow: &xlsxColor{Theme: intPtr(4), Tint: -0.249977111117893},
}, // 12 }, // 12
{ {
ColorSeries: &xlsxTabColor{Theme: 4}, ColorSeries: &xlsxColor{Theme: intPtr(4)},
ColorNegative: &xlsxTabColor{Theme: 5}, ColorNegative: &xlsxColor{Theme: intPtr(5)},
ColorMarkers: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893}, ColorMarkers: &xlsxColor{Theme: intPtr(4), Tint: -0.249977111117893},
ColorFirst: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893}, ColorFirst: &xlsxColor{Theme: intPtr(4), Tint: -0.249977111117893},
ColorLast: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893}, ColorLast: &xlsxColor{Theme: intPtr(4), Tint: -0.249977111117893},
ColorHigh: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893}, ColorHigh: &xlsxColor{Theme: intPtr(4), Tint: -0.249977111117893},
ColorLow: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893}, ColorLow: &xlsxColor{Theme: intPtr(4), Tint: -0.249977111117893},
}, // 13 }, // 13
{ {
ColorSeries: &xlsxTabColor{Theme: 5}, ColorSeries: &xlsxColor{Theme: intPtr(5)},
ColorNegative: &xlsxTabColor{Theme: 6}, ColorNegative: &xlsxColor{Theme: intPtr(6)},
ColorMarkers: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893}, ColorMarkers: &xlsxColor{Theme: intPtr(5), Tint: -0.249977111117893},
ColorFirst: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893}, ColorFirst: &xlsxColor{Theme: intPtr(5), Tint: -0.249977111117893},
ColorLast: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893}, ColorLast: &xlsxColor{Theme: intPtr(5), Tint: -0.249977111117893},
ColorHigh: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893}, ColorHigh: &xlsxColor{Theme: intPtr(5), Tint: -0.249977111117893},
ColorLow: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893}, ColorLow: &xlsxColor{Theme: intPtr(5), Tint: -0.249977111117893},
}, // 14 }, // 14
{ {
ColorSeries: &xlsxTabColor{Theme: 6}, ColorSeries: &xlsxColor{Theme: intPtr(6)},
ColorNegative: &xlsxTabColor{Theme: 7}, ColorNegative: &xlsxColor{Theme: intPtr(7)},
ColorMarkers: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893}, ColorMarkers: &xlsxColor{Theme: intPtr(6), Tint: -0.249977111117893},
ColorFirst: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893}, ColorFirst: &xlsxColor{Theme: intPtr(6), Tint: -0.249977111117893},
ColorLast: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893}, ColorLast: &xlsxColor{Theme: intPtr(6), Tint: -0.249977111117893},
ColorHigh: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893}, ColorHigh: &xlsxColor{Theme: intPtr(6), Tint: -0.249977111117893},
ColorLow: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893}, ColorLow: &xlsxColor{Theme: intPtr(6), Tint: -0.249977111117893},
}, // 15 }, // 15
{ {
ColorSeries: &xlsxTabColor{Theme: 7}, ColorSeries: &xlsxColor{Theme: intPtr(7)},
ColorNegative: &xlsxTabColor{Theme: 8}, ColorNegative: &xlsxColor{Theme: intPtr(8)},
ColorMarkers: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893}, ColorMarkers: &xlsxColor{Theme: intPtr(7), Tint: -0.249977111117893},
ColorFirst: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893}, ColorFirst: &xlsxColor{Theme: intPtr(7), Tint: -0.249977111117893},
ColorLast: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893}, ColorLast: &xlsxColor{Theme: intPtr(7), Tint: -0.249977111117893},
ColorHigh: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893}, ColorHigh: &xlsxColor{Theme: intPtr(7), Tint: -0.249977111117893},
ColorLow: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893}, ColorLow: &xlsxColor{Theme: intPtr(7), Tint: -0.249977111117893},
}, // 16 }, // 16
{ {
ColorSeries: &xlsxTabColor{Theme: 8}, ColorSeries: &xlsxColor{Theme: intPtr(8)},
ColorNegative: &xlsxTabColor{Theme: 9}, ColorNegative: &xlsxColor{Theme: intPtr(9)},
ColorMarkers: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893}, ColorMarkers: &xlsxColor{Theme: intPtr(8), Tint: -0.249977111117893},
ColorFirst: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893}, ColorFirst: &xlsxColor{Theme: intPtr(8), Tint: -0.249977111117893},
ColorLast: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893}, ColorLast: &xlsxColor{Theme: intPtr(8), Tint: -0.249977111117893},
ColorHigh: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893}, ColorHigh: &xlsxColor{Theme: intPtr(8), Tint: -0.249977111117893},
ColorLow: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893}, ColorLow: &xlsxColor{Theme: intPtr(8), Tint: -0.249977111117893},
}, // 17 }, // 17
{ {
ColorSeries: &xlsxTabColor{Theme: 9}, ColorSeries: &xlsxColor{Theme: intPtr(9)},
ColorNegative: &xlsxTabColor{Theme: 4}, ColorNegative: &xlsxColor{Theme: intPtr(4)},
ColorMarkers: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893}, ColorMarkers: &xlsxColor{Theme: intPtr(9), Tint: -0.249977111117893},
ColorFirst: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893}, ColorFirst: &xlsxColor{Theme: intPtr(9), Tint: -0.249977111117893},
ColorLast: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893}, ColorLast: &xlsxColor{Theme: intPtr(9), Tint: -0.249977111117893},
ColorHigh: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893}, ColorHigh: &xlsxColor{Theme: intPtr(9), Tint: -0.249977111117893},
ColorLow: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893}, ColorLow: &xlsxColor{Theme: intPtr(9), Tint: -0.249977111117893},
}, // 18 }, // 18
{ {
ColorSeries: &xlsxTabColor{Theme: 4, Tint: 0.39997558519241921}, ColorSeries: &xlsxColor{Theme: intPtr(4), Tint: 0.39997558519241921},
ColorNegative: &xlsxTabColor{Theme: 0, Tint: -0.499984740745262}, ColorNegative: &xlsxColor{Theme: intPtr(0), Tint: -0.499984740745262},
ColorMarkers: &xlsxTabColor{Theme: 4, Tint: 0.79998168889431442}, ColorMarkers: &xlsxColor{Theme: intPtr(4), Tint: 0.79998168889431442},
ColorFirst: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893}, ColorFirst: &xlsxColor{Theme: intPtr(4), Tint: -0.249977111117893},
ColorLast: &xlsxTabColor{Theme: 4, Tint: -0.249977111117893}, ColorLast: &xlsxColor{Theme: intPtr(4), Tint: -0.249977111117893},
ColorHigh: &xlsxTabColor{Theme: 4, Tint: -0.499984740745262}, ColorHigh: &xlsxColor{Theme: intPtr(4), Tint: -0.499984740745262},
ColorLow: &xlsxTabColor{Theme: 4, Tint: -0.499984740745262}, ColorLow: &xlsxColor{Theme: intPtr(4), Tint: -0.499984740745262},
}, // 19 }, // 19
{ {
ColorSeries: &xlsxTabColor{Theme: 5, Tint: 0.39997558519241921}, ColorSeries: &xlsxColor{Theme: intPtr(5), Tint: 0.39997558519241921},
ColorNegative: &xlsxTabColor{Theme: 0, Tint: -0.499984740745262}, ColorNegative: &xlsxColor{Theme: intPtr(0), Tint: -0.499984740745262},
ColorMarkers: &xlsxTabColor{Theme: 5, Tint: 0.79998168889431442}, ColorMarkers: &xlsxColor{Theme: intPtr(5), Tint: 0.79998168889431442},
ColorFirst: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893}, ColorFirst: &xlsxColor{Theme: intPtr(5), Tint: -0.249977111117893},
ColorLast: &xlsxTabColor{Theme: 5, Tint: -0.249977111117893}, ColorLast: &xlsxColor{Theme: intPtr(5), Tint: -0.249977111117893},
ColorHigh: &xlsxTabColor{Theme: 5, Tint: -0.499984740745262}, ColorHigh: &xlsxColor{Theme: intPtr(5), Tint: -0.499984740745262},
ColorLow: &xlsxTabColor{Theme: 5, Tint: -0.499984740745262}, ColorLow: &xlsxColor{Theme: intPtr(5), Tint: -0.499984740745262},
}, // 20 }, // 20
{ {
ColorSeries: &xlsxTabColor{Theme: 6, Tint: 0.39997558519241921}, ColorSeries: &xlsxColor{Theme: intPtr(6), Tint: 0.39997558519241921},
ColorNegative: &xlsxTabColor{Theme: 0, Tint: -0.499984740745262}, ColorNegative: &xlsxColor{Theme: intPtr(0), Tint: -0.499984740745262},
ColorMarkers: &xlsxTabColor{Theme: 6, Tint: 0.79998168889431442}, ColorMarkers: &xlsxColor{Theme: intPtr(6), Tint: 0.79998168889431442},
ColorFirst: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893}, ColorFirst: &xlsxColor{Theme: intPtr(6), Tint: -0.249977111117893},
ColorLast: &xlsxTabColor{Theme: 6, Tint: -0.249977111117893}, ColorLast: &xlsxColor{Theme: intPtr(6), Tint: -0.249977111117893},
ColorHigh: &xlsxTabColor{Theme: 6, Tint: -0.499984740745262}, ColorHigh: &xlsxColor{Theme: intPtr(6), Tint: -0.499984740745262},
ColorLow: &xlsxTabColor{Theme: 6, Tint: -0.499984740745262}, ColorLow: &xlsxColor{Theme: intPtr(6), Tint: -0.499984740745262},
}, // 21 }, // 21
{ {
ColorSeries: &xlsxTabColor{Theme: 7, Tint: 0.39997558519241921}, ColorSeries: &xlsxColor{Theme: intPtr(7), Tint: 0.39997558519241921},
ColorNegative: &xlsxTabColor{Theme: 0, Tint: -0.499984740745262}, ColorNegative: &xlsxColor{Theme: intPtr(0), Tint: -0.499984740745262},
ColorMarkers: &xlsxTabColor{Theme: 7, Tint: 0.79998168889431442}, ColorMarkers: &xlsxColor{Theme: intPtr(7), Tint: 0.79998168889431442},
ColorFirst: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893}, ColorFirst: &xlsxColor{Theme: intPtr(7), Tint: -0.249977111117893},
ColorLast: &xlsxTabColor{Theme: 7, Tint: -0.249977111117893}, ColorLast: &xlsxColor{Theme: intPtr(7), Tint: -0.249977111117893},
ColorHigh: &xlsxTabColor{Theme: 7, Tint: -0.499984740745262}, ColorHigh: &xlsxColor{Theme: intPtr(7), Tint: -0.499984740745262},
ColorLow: &xlsxTabColor{Theme: 7, Tint: -0.499984740745262}, ColorLow: &xlsxColor{Theme: intPtr(7), Tint: -0.499984740745262},
}, // 22 }, // 22
{ {
ColorSeries: &xlsxTabColor{Theme: 8, Tint: 0.39997558519241921}, ColorSeries: &xlsxColor{Theme: intPtr(8), Tint: 0.39997558519241921},
ColorNegative: &xlsxTabColor{Theme: 0, Tint: -0.499984740745262}, ColorNegative: &xlsxColor{Theme: intPtr(0), Tint: -0.499984740745262},
ColorMarkers: &xlsxTabColor{Theme: 8, Tint: 0.79998168889431442}, ColorMarkers: &xlsxColor{Theme: intPtr(8), Tint: 0.79998168889431442},
ColorFirst: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893}, ColorFirst: &xlsxColor{Theme: intPtr(8), Tint: -0.249977111117893},
ColorLast: &xlsxTabColor{Theme: 8, Tint: -0.249977111117893}, ColorLast: &xlsxColor{Theme: intPtr(8), Tint: -0.249977111117893},
ColorHigh: &xlsxTabColor{Theme: 8, Tint: -0.499984740745262}, ColorHigh: &xlsxColor{Theme: intPtr(8), Tint: -0.499984740745262},
ColorLow: &xlsxTabColor{Theme: 8, Tint: -0.499984740745262}, ColorLow: &xlsxColor{Theme: intPtr(8), Tint: -0.499984740745262},
}, // 23 }, // 23
{ {
ColorSeries: &xlsxTabColor{Theme: 9, Tint: 0.39997558519241921}, ColorSeries: &xlsxColor{Theme: intPtr(9), Tint: 0.39997558519241921},
ColorNegative: &xlsxTabColor{Theme: 0, Tint: -0.499984740745262}, ColorNegative: &xlsxColor{Theme: intPtr(0), Tint: -0.499984740745262},
ColorMarkers: &xlsxTabColor{Theme: 9, Tint: 0.79998168889431442}, ColorMarkers: &xlsxColor{Theme: intPtr(9), Tint: 0.79998168889431442},
ColorFirst: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893}, ColorFirst: &xlsxColor{Theme: intPtr(9), Tint: -0.249977111117893},
ColorLast: &xlsxTabColor{Theme: 9, Tint: -0.249977111117893}, ColorLast: &xlsxColor{Theme: intPtr(9), Tint: -0.249977111117893},
ColorHigh: &xlsxTabColor{Theme: 9, Tint: -0.499984740745262}, ColorHigh: &xlsxColor{Theme: intPtr(9), Tint: -0.499984740745262},
ColorLow: &xlsxTabColor{Theme: 9, Tint: -0.499984740745262}, ColorLow: &xlsxColor{Theme: intPtr(9), Tint: -0.499984740745262},
}, // 24 }, // 24
{ {
ColorSeries: &xlsxTabColor{Theme: 1, Tint: 0.499984740745262}, ColorSeries: &xlsxColor{Theme: intPtr(1), Tint: 0.499984740745262},
ColorNegative: &xlsxTabColor{Theme: 1, Tint: 0.249977111117893}, ColorNegative: &xlsxColor{Theme: intPtr(1), Tint: 0.249977111117893},
ColorMarkers: &xlsxTabColor{Theme: 1, Tint: 0.249977111117893}, ColorMarkers: &xlsxColor{Theme: intPtr(1), Tint: 0.249977111117893},
ColorFirst: &xlsxTabColor{Theme: 1, Tint: 0.249977111117893}, ColorFirst: &xlsxColor{Theme: intPtr(1), Tint: 0.249977111117893},
ColorLast: &xlsxTabColor{Theme: 1, Tint: 0.249977111117893}, ColorLast: &xlsxColor{Theme: intPtr(1), Tint: 0.249977111117893},
ColorHigh: &xlsxTabColor{Theme: 1, Tint: 0.249977111117893}, ColorHigh: &xlsxColor{Theme: intPtr(1), Tint: 0.249977111117893},
ColorLow: &xlsxTabColor{Theme: 1, Tint: 0.249977111117893}, ColorLow: &xlsxColor{Theme: intPtr(1), Tint: 0.249977111117893},
}, // 25 }, // 25
{ {
ColorSeries: &xlsxTabColor{Theme: 1, Tint: 0.34998626667073579}, ColorSeries: &xlsxColor{Theme: intPtr(1), Tint: 0.34998626667073579},
ColorNegative: &xlsxTabColor{Theme: 0, Tint: 0.249977111117893}, ColorNegative: &xlsxColor{Theme: intPtr(0), Tint: 0.249977111117893},
ColorMarkers: &xlsxTabColor{Theme: 0, Tint: 0.249977111117893}, ColorMarkers: &xlsxColor{Theme: intPtr(0), Tint: 0.249977111117893},
ColorFirst: &xlsxTabColor{Theme: 0, Tint: 0.249977111117893}, ColorFirst: &xlsxColor{Theme: intPtr(0), Tint: 0.249977111117893},
ColorLast: &xlsxTabColor{Theme: 0, Tint: 0.249977111117893}, ColorLast: &xlsxColor{Theme: intPtr(0), Tint: 0.249977111117893},
ColorHigh: &xlsxTabColor{Theme: 0, Tint: 0.249977111117893}, ColorHigh: &xlsxColor{Theme: intPtr(0), Tint: 0.249977111117893},
ColorLow: &xlsxTabColor{Theme: 0, Tint: 0.249977111117893}, ColorLow: &xlsxColor{Theme: intPtr(0), Tint: 0.249977111117893},
}, // 26 }, // 26
{ {
ColorSeries: &xlsxTabColor{RGB: "FF323232"}, ColorSeries: &xlsxColor{RGB: "FF323232"},
ColorNegative: &xlsxTabColor{RGB: "FFD00000"}, ColorNegative: &xlsxColor{RGB: "FFD00000"},
ColorMarkers: &xlsxTabColor{RGB: "FFD00000"}, ColorMarkers: &xlsxColor{RGB: "FFD00000"},
ColorFirst: &xlsxTabColor{RGB: "FFD00000"}, ColorFirst: &xlsxColor{RGB: "FFD00000"},
ColorLast: &xlsxTabColor{RGB: "FFD00000"}, ColorLast: &xlsxColor{RGB: "FFD00000"},
ColorHigh: &xlsxTabColor{RGB: "FFD00000"}, ColorHigh: &xlsxColor{RGB: "FFD00000"},
ColorLow: &xlsxTabColor{RGB: "FFD00000"}, ColorLow: &xlsxColor{RGB: "FFD00000"},
}, // 27 }, // 27
{ {
ColorSeries: &xlsxTabColor{RGB: "FF000000"}, ColorSeries: &xlsxColor{RGB: "FF000000"},
ColorNegative: &xlsxTabColor{RGB: "FF0070C0"}, ColorNegative: &xlsxColor{RGB: "FF0070C0"},
ColorMarkers: &xlsxTabColor{RGB: "FF0070C0"}, ColorMarkers: &xlsxColor{RGB: "FF0070C0"},
ColorFirst: &xlsxTabColor{RGB: "FF0070C0"}, ColorFirst: &xlsxColor{RGB: "FF0070C0"},
ColorLast: &xlsxTabColor{RGB: "FF0070C0"}, ColorLast: &xlsxColor{RGB: "FF0070C0"},
ColorHigh: &xlsxTabColor{RGB: "FF0070C0"}, ColorHigh: &xlsxColor{RGB: "FF0070C0"},
ColorLow: &xlsxTabColor{RGB: "FF0070C0"}, ColorLow: &xlsxColor{RGB: "FF0070C0"},
}, // 28 }, // 28
{ {
ColorSeries: &xlsxTabColor{RGB: "FF376092"}, ColorSeries: &xlsxColor{RGB: "FF376092"},
ColorNegative: &xlsxTabColor{RGB: "FFD00000"}, ColorNegative: &xlsxColor{RGB: "FFD00000"},
ColorMarkers: &xlsxTabColor{RGB: "FFD00000"}, ColorMarkers: &xlsxColor{RGB: "FFD00000"},
ColorFirst: &xlsxTabColor{RGB: "FFD00000"}, ColorFirst: &xlsxColor{RGB: "FFD00000"},
ColorLast: &xlsxTabColor{RGB: "FFD00000"}, ColorLast: &xlsxColor{RGB: "FFD00000"},
ColorHigh: &xlsxTabColor{RGB: "FFD00000"}, ColorHigh: &xlsxColor{RGB: "FFD00000"},
ColorLow: &xlsxTabColor{RGB: "FFD00000"}, ColorLow: &xlsxColor{RGB: "FFD00000"},
}, // 29 }, // 29
{ {
ColorSeries: &xlsxTabColor{RGB: "FF0070C0"}, ColorSeries: &xlsxColor{RGB: "FF0070C0"},
ColorNegative: &xlsxTabColor{RGB: "FF000000"}, ColorNegative: &xlsxColor{RGB: "FF000000"},
ColorMarkers: &xlsxTabColor{RGB: "FF000000"}, ColorMarkers: &xlsxColor{RGB: "FF000000"},
ColorFirst: &xlsxTabColor{RGB: "FF000000"}, ColorFirst: &xlsxColor{RGB: "FF000000"},
ColorLast: &xlsxTabColor{RGB: "FF000000"}, ColorLast: &xlsxColor{RGB: "FF000000"},
ColorHigh: &xlsxTabColor{RGB: "FF000000"}, ColorHigh: &xlsxColor{RGB: "FF000000"},
ColorLow: &xlsxTabColor{RGB: "FF000000"}, ColorLow: &xlsxColor{RGB: "FF000000"},
}, // 30 }, // 30
{ {
ColorSeries: &xlsxTabColor{RGB: "FF5F5F5F"}, ColorSeries: &xlsxColor{RGB: "FF5F5F5F"},
ColorNegative: &xlsxTabColor{RGB: "FFFFB620"}, ColorNegative: &xlsxColor{RGB: "FFFFB620"},
ColorMarkers: &xlsxTabColor{RGB: "FFD70077"}, ColorMarkers: &xlsxColor{RGB: "FFD70077"},
ColorFirst: &xlsxTabColor{RGB: "FF5687C2"}, ColorFirst: &xlsxColor{RGB: "FF5687C2"},
ColorLast: &xlsxTabColor{RGB: "FF359CEB"}, ColorLast: &xlsxColor{RGB: "FF359CEB"},
ColorHigh: &xlsxTabColor{RGB: "FF56BE79"}, ColorHigh: &xlsxColor{RGB: "FF56BE79"},
ColorLow: &xlsxTabColor{RGB: "FFFF5055"}, ColorLow: &xlsxColor{RGB: "FFFF5055"},
}, // 31 }, // 31
{ {
ColorSeries: &xlsxTabColor{RGB: "FF5687C2"}, ColorSeries: &xlsxColor{RGB: "FF5687C2"},
ColorNegative: &xlsxTabColor{RGB: "FFFFB620"}, ColorNegative: &xlsxColor{RGB: "FFFFB620"},
ColorMarkers: &xlsxTabColor{RGB: "FFD70077"}, ColorMarkers: &xlsxColor{RGB: "FFD70077"},
ColorFirst: &xlsxTabColor{RGB: "FF777777"}, ColorFirst: &xlsxColor{RGB: "FF777777"},
ColorLast: &xlsxTabColor{RGB: "FF359CEB"}, ColorLast: &xlsxColor{RGB: "FF359CEB"},
ColorHigh: &xlsxTabColor{RGB: "FF56BE79"}, ColorHigh: &xlsxColor{RGB: "FF56BE79"},
ColorLow: &xlsxTabColor{RGB: "FFFF5055"}, ColorLow: &xlsxColor{RGB: "FFFF5055"},
}, // 32 }, // 32
{ {
ColorSeries: &xlsxTabColor{RGB: "FFC6EFCE"}, ColorSeries: &xlsxColor{RGB: "FFC6EFCE"},
ColorNegative: &xlsxTabColor{RGB: "FFFFC7CE"}, ColorNegative: &xlsxColor{RGB: "FFFFC7CE"},
ColorMarkers: &xlsxTabColor{RGB: "FF8CADD6"}, ColorMarkers: &xlsxColor{RGB: "FF8CADD6"},
ColorFirst: &xlsxTabColor{RGB: "FFFFDC47"}, ColorFirst: &xlsxColor{RGB: "FFFFDC47"},
ColorLast: &xlsxTabColor{RGB: "FFFFEB9C"}, ColorLast: &xlsxColor{RGB: "FFFFEB9C"},
ColorHigh: &xlsxTabColor{RGB: "FF60D276"}, ColorHigh: &xlsxColor{RGB: "FF60D276"},
ColorLow: &xlsxTabColor{RGB: "FFFF5367"}, ColorLow: &xlsxColor{RGB: "FFFF5367"},
}, // 33 }, // 33
{ {
ColorSeries: &xlsxTabColor{RGB: "FF00B050"}, ColorSeries: &xlsxColor{RGB: "FF00B050"},
ColorNegative: &xlsxTabColor{RGB: "FFFF0000"}, ColorNegative: &xlsxColor{RGB: "FFFF0000"},
ColorMarkers: &xlsxTabColor{RGB: "FF0070C0"}, ColorMarkers: &xlsxColor{RGB: "FF0070C0"},
ColorFirst: &xlsxTabColor{RGB: "FFFFC000"}, ColorFirst: &xlsxColor{RGB: "FFFFC000"},
ColorLast: &xlsxTabColor{RGB: "FFFFC000"}, ColorLast: &xlsxColor{RGB: "FFFFC000"},
ColorHigh: &xlsxTabColor{RGB: "FF00B050"}, ColorHigh: &xlsxColor{RGB: "FF00B050"},
ColorLow: &xlsxTabColor{RGB: "FFFF0000"}, ColorLow: &xlsxColor{RGB: "FFFF0000"},
}, // 34 }, // 34
{ {
ColorSeries: &xlsxTabColor{Theme: 3}, ColorSeries: &xlsxColor{Theme: intPtr(3)},
ColorNegative: &xlsxTabColor{Theme: 9}, ColorNegative: &xlsxColor{Theme: intPtr(9)},
ColorMarkers: &xlsxTabColor{Theme: 8}, ColorMarkers: &xlsxColor{Theme: intPtr(8)},
ColorFirst: &xlsxTabColor{Theme: 4}, ColorFirst: &xlsxColor{Theme: intPtr(4)},
ColorLast: &xlsxTabColor{Theme: 5}, ColorLast: &xlsxColor{Theme: intPtr(5)},
ColorHigh: &xlsxTabColor{Theme: 6}, ColorHigh: &xlsxColor{Theme: intPtr(6)},
ColorLow: &xlsxTabColor{Theme: 7}, ColorLow: &xlsxColor{Theme: intPtr(7)},
}, // 35 }, // 35
{ {
ColorSeries: &xlsxTabColor{Theme: 1}, ColorSeries: &xlsxColor{Theme: intPtr(1)},
ColorNegative: &xlsxTabColor{Theme: 9}, ColorNegative: &xlsxColor{Theme: intPtr(9)},
ColorMarkers: &xlsxTabColor{Theme: 8}, ColorMarkers: &xlsxColor{Theme: intPtr(8)},
ColorFirst: &xlsxTabColor{Theme: 4}, ColorFirst: &xlsxColor{Theme: intPtr(4)},
ColorLast: &xlsxTabColor{Theme: 5}, ColorLast: &xlsxColor{Theme: intPtr(5)},
ColorHigh: &xlsxTabColor{Theme: 6}, ColorHigh: &xlsxColor{Theme: intPtr(6)},
ColorLow: &xlsxTabColor{Theme: 7}, ColorLow: &xlsxColor{Theme: intPtr(7)},
}, // 36 }, // 36
} }
return groups[ID]
} }
// AddSparkline provides a function to add sparklines to the worksheet by // AddSparkline provides a function to add sparklines to the worksheet by
// given formatting options. Sparklines are small charts that fit in a single // given formatting options. Sparklines are small charts that fit in a single
// cell and are used to show trends in data. Sparklines are a feature of Excel // cell and are used to show trends in data. Sparklines are a feature of Excel
// 2010 and later only. You can write them to an XLSX file that can be read by // 2010 and later only. You can write them to workbook that can be read by Excel
// Excel 2007, but they won't be displayed. For example, add a grouped // 2007, but they won't be displayed. For example, add a grouped sparkline.
// sparkline. Changes are applied to all three: // Changes are applied to all three:
// //
// err := f.AddSparkline("Sheet1", &excelize.SparklineOptions{ // err := f.AddSparkline("Sheet1", &excelize.SparklineOptions{
// Location: []string{"A1", "A2", "A3"}, // Location: []string{"A1", "A2", "A3"},
@ -415,7 +414,7 @@ func (f *File) AddSparkline(sheet string, opts *SparklineOptions) error {
} }
sparkType = specifiedSparkTypes sparkType = specifiedSparkTypes
} }
group = f.addSparklineGroupByStyle(opts.Style) group = getSparklineGroupPresets()[opts.Style]
group.Type = sparkType group.Type = sparkType
group.ColorAxis = &xlsxColor{RGB: "FF000000"} group.ColorAxis = &xlsxColor{RGB: "FF000000"}
group.DisplayEmptyCellsAs = "gap" group.DisplayEmptyCellsAs = "gap"
@ -427,7 +426,7 @@ func (f *File) AddSparkline(sheet string, opts *SparklineOptions) error {
group.DisplayXAxis = opts.Axis group.DisplayXAxis = opts.Axis
group.Markers = opts.Markers group.Markers = opts.Markers
if opts.SeriesColor != "" { if opts.SeriesColor != "" {
group.ColorSeries = &xlsxTabColor{ group.ColorSeries = &xlsxColor{
RGB: getPaletteColor(opts.SeriesColor), RGB: getPaletteColor(opts.SeriesColor),
} }
} }
@ -489,9 +488,9 @@ func (f *File) appendSparkline(ws *xlsxWorksheet, group *xlsxX14SparklineGroup,
err error err error
idx int idx int
appendMode bool appendMode bool
decodeExtLst = new(decodeWorksheetExt) decodeExtLst = new(decodeExtLst)
decodeSparklineGroups *decodeX14SparklineGroups decodeSparklineGroups *decodeX14SparklineGroups
ext *xlsxWorksheetExt ext *xlsxExt
sparklineGroupsBytes, sparklineGroupBytes, extLstBytes []byte sparklineGroupsBytes, sparklineGroupBytes, extLstBytes []byte
) )
sparklineGroupBytes, _ = xml.Marshal(group) sparklineGroupBytes, _ = xml.Marshal(group)
@ -523,13 +522,13 @@ func (f *File) appendSparkline(ws *xlsxWorksheet, group *xlsxX14SparklineGroup,
XMLNSXM: NameSpaceSpreadSheetExcel2006Main.Value, XMLNSXM: NameSpaceSpreadSheetExcel2006Main.Value,
SparklineGroups: []*xlsxX14SparklineGroup{group}, SparklineGroups: []*xlsxX14SparklineGroup{group},
}) })
decodeExtLst.Ext = append(decodeExtLst.Ext, &xlsxWorksheetExt{ decodeExtLst.Ext = append(decodeExtLst.Ext, &xlsxExt{
URI: ExtURISparklineGroups, Content: string(sparklineGroupsBytes), URI: ExtURISparklineGroups, Content: string(sparklineGroupsBytes),
}) })
} }
sort.Slice(decodeExtLst.Ext, func(i, j int) bool { sort.Slice(decodeExtLst.Ext, func(i, j int) bool {
return inStrSlice(extensionURIPriority, decodeExtLst.Ext[i].URI, false) < return inStrSlice(worksheetExtURIPriority, decodeExtLst.Ext[i].URI, false) <
inStrSlice(extensionURIPriority, decodeExtLst.Ext[j].URI, false) inStrSlice(worksheetExtURIPriority, decodeExtLst.Ext[j].URI, false)
}) })
extLstBytes, err = xml.Marshal(decodeExtLst) extLstBytes, err = xml.Marshal(decodeExtLst)
ws.ExtLst = &xlsxExtLst{Ext: strings.TrimSuffix(strings.TrimPrefix(string(extLstBytes), "<extLst>"), "</extLst>")} ws.ExtLst = &xlsxExtLst{Ext: strings.TrimSuffix(strings.TrimPrefix(string(extLstBytes), "<extLst>"), "</extLst>")}

View File

@ -224,59 +224,57 @@ func TestAddSparkline(t *testing.T) {
Range: []string{"Sheet2!A3:E3"}, Range: []string{"Sheet2!A3:E3"},
}), "sheet SheetN does not exist") }), "sheet SheetN does not exist")
assert.EqualError(t, f.AddSparkline("Sheet1", nil), ErrParameterRequired.Error()) assert.Equal(t, ErrParameterRequired, f.AddSparkline("Sheet1", nil))
// Test add sparkline with invalid sheet name // Test add sparkline with invalid sheet name
assert.EqualError(t, f.AddSparkline("Sheet:1", &SparklineOptions{ assert.Equal(t, ErrSheetNameInvalid, f.AddSparkline("Sheet:1", &SparklineOptions{
Location: []string{"F3"}, Location: []string{"F3"},
Range: []string{"Sheet2!A3:E3"}, Range: []string{"Sheet2!A3:E3"},
Type: "win_loss", Type: "win_loss",
Negative: true, Negative: true,
}), ErrSheetNameInvalid.Error()) }))
assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOptions{ assert.Equal(t, ErrSparklineLocation, f.AddSparkline("Sheet1", &SparklineOptions{
Range: []string{"Sheet2!A3:E3"}, Range: []string{"Sheet2!A3:E3"},
}), ErrSparklineLocation.Error()) }))
assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOptions{ assert.Equal(t, ErrSparklineRange, f.AddSparkline("Sheet1", &SparklineOptions{
Location: []string{"F3"}, Location: []string{"F3"},
}), ErrSparklineRange.Error()) }))
assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOptions{ assert.Equal(t, ErrSparkline, f.AddSparkline("Sheet1", &SparklineOptions{
Location: []string{"F2", "F3"}, Location: []string{"F2", "F3"},
Range: []string{"Sheet2!A3:E3"}, Range: []string{"Sheet2!A3:E3"},
}), ErrSparkline.Error()) }))
assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOptions{ assert.Equal(t, ErrSparklineType, f.AddSparkline("Sheet1", &SparklineOptions{
Location: []string{"F3"}, Location: []string{"F3"},
Range: []string{"Sheet2!A3:E3"}, Range: []string{"Sheet2!A3:E3"},
Type: "unknown_type", Type: "unknown_type",
}), ErrSparklineType.Error()) }))
assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOptions{ assert.Equal(t, ErrSparklineStyle, f.AddSparkline("Sheet1", &SparklineOptions{
Location: []string{"F3"}, Location: []string{"F3"},
Range: []string{"Sheet2!A3:E3"}, Range: []string{"Sheet2!A3:E3"},
Style: -1, Style: -1,
}), ErrSparklineStyle.Error()) }))
assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOptions{ assert.Equal(t, ErrSparklineStyle, f.AddSparkline("Sheet1", &SparklineOptions{
Location: []string{"F3"}, Location: []string{"F3"},
Range: []string{"Sheet2!A3:E3"}, Range: []string{"Sheet2!A3:E3"},
Style: -1, Style: -1,
}), ErrSparklineStyle.Error()) }))
// Test creating a conditional format with existing extension lists // Test creating a conditional format with existing extension lists
ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml") ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
assert.True(t, ok) assert.True(t, ok)
ws.(*xlsxWorksheet).ExtLst = &xlsxExtLst{Ext: ` ws.(*xlsxWorksheet).ExtLst = &xlsxExtLst{Ext: fmt.Sprintf(`<ext uri="%s"><x14:slicerList /></ext><ext uri="%s"><x14:sparklineGroups /></ext>`, ExtURISlicerListX14, ExtURISparklineGroups)}
<ext uri="{A8765BA9-456A-4dab-B4F3-ACF838C121DE}"><x14:slicerList /></ext>
<ext uri="{05C60535-1F16-4fd2-B633-F4F36F0B64E0}"><x14:sparklineGroups /></ext>`}
assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOptions{ assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOptions{
Location: []string{"A3"}, Location: []string{"A3"},
Range: []string{"Sheet3!A2:J2"}, Range: []string{"Sheet3!A2:J2"},
Type: "column", Type: "column",
})) }))
// Test creating a conditional format with invalid extension list characters // Test creating a conditional format with invalid extension list characters
ws.(*xlsxWorksheet).ExtLst.Ext = `<ext uri="{05C60535-1F16-4fd2-B633-F4F36F0B64E0}"><x14:sparklineGroups><x14:sparklineGroup></x14:sparklines></x14:sparklineGroup></x14:sparklineGroups></ext>` ws.(*xlsxWorksheet).ExtLst.Ext = fmt.Sprintf(`<ext uri="%s"><x14:sparklineGroups><x14:sparklineGroup></x14:sparklines></x14:sparklineGroup></x14:sparklineGroups></ext>`, ExtURISparklineGroups)
assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOptions{ assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOptions{
Location: []string{"A2"}, Location: []string{"A2"},
Range: []string{"Sheet3!A1:J1"}, Range: []string{"Sheet3!A1:J1"},

View File

@ -1,4 +1,4 @@
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in // this source code is governed by a BSD-style license that can be found in
// the LICENSE file. // the LICENSE file.
// //
@ -7,7 +7,7 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming // Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of // API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.16 or later. // data. This library needs Go version 1.18 or later.
package excelize package excelize
@ -38,14 +38,16 @@ type StreamWriter struct {
tableParts string tableParts string
} }
// NewStreamWriter return stream writer struct by given worksheet name for // NewStreamWriter returns stream writer struct by given worksheet name used for
// generate new worksheet with large amounts of data. Note that after set // writing data on a new existing empty worksheet with large amounts of data.
// rows, you must call the 'Flush' method to end the streaming writing process // Note that after writing data with the stream writer for the worksheet, you
// and ensure that the order of row numbers is ascending, the normal mode // must call the 'Flush' method to end the streaming writing process, ensure
// functions and stream mode functions can't be work mixed to writing data on // that the order of row numbers is ascending when set rows, and the normal
// the worksheets, you can't get cell value when in-memory chunks data over // mode functions and stream mode functions can not be work mixed to writing
// 16MB. For example, set data for worksheet of size 102400 rows x 50 columns // data on the worksheets. The stream writer will try to use temporary files on
// with numbers and style: // disk to reduce the memory usage when in-memory chunks data over 16MB, and
// you can't get cell value at this time. For example, set data for worksheet
// of size 102400 rows x 50 columns with numbers and style:
// //
// f := excelize.NewFile() // f := excelize.NewFile()
// defer func() { // defer func() {
@ -116,7 +118,7 @@ func (f *File) NewStreamWriter(sheet string) (*StreamWriter, error) {
} }
sheetID := f.getSheetID(sheet) sheetID := f.getSheetID(sheet)
if sheetID == -1 { if sheetID == -1 {
return nil, newNoExistSheetError(sheet) return nil, ErrSheetNotExist{sheet}
} }
sw := &StreamWriter{ sw := &StreamWriter{
file: f, file: f,
@ -182,7 +184,7 @@ func (sw *StreamWriter) AddTable(table *Table) error {
} }
// Correct table reference range, such correct C1:B3 to B1:C3. // Correct table reference range, such correct C1:B3 to B1:C3.
ref, err := sw.file.coordinatesToRangeRef(coordinates) ref, err := coordinatesToRangeRef(coordinates)
if err != nil { if err != nil {
return err return err
} }
@ -288,7 +290,7 @@ func (sw *StreamWriter) getRowValues(hRow, hCol, vCol int) (res []string, err er
} }
} }
// Check if the token is an XLSX row with the matching row number. // Check if the token is an worksheet row with the matching row number.
func getRowElement(token xml.Token, hRow int) (startElement xml.StartElement, ok bool) { func getRowElement(token xml.Token, hRow int) (startElement xml.StartElement, ok bool) {
startElement, ok = token.(xml.StartElement) startElement, ok = token.(xml.StartElement)
if !ok { if !ok {
@ -437,24 +439,24 @@ func (sw *StreamWriter) SetRow(cell string, values []interface{}, opts ...RowOpt
// the width column B:C as 20: // the width column B:C as 20:
// //
// err := sw.SetColWidth(2, 3, 20) // err := sw.SetColWidth(2, 3, 20)
func (sw *StreamWriter) SetColWidth(min, max int, width float64) error { func (sw *StreamWriter) SetColWidth(minVal, maxVal int, width float64) error {
if sw.sheetWritten { if sw.sheetWritten {
return ErrStreamSetColWidth return ErrStreamSetColWidth
} }
if min < MinColumns || min > MaxColumns || max < MinColumns || max > MaxColumns { if minVal < MinColumns || minVal > MaxColumns || maxVal < MinColumns || maxVal > MaxColumns {
return ErrColumnNumber return ErrColumnNumber
} }
if width > MaxColumnWidth { if width > MaxColumnWidth {
return ErrColumnWidth return ErrColumnWidth
} }
if min > max { if minVal > maxVal {
min, max = max, min minVal, maxVal = maxVal, minVal
} }
sw.cols.WriteString(`<col min="`) sw.cols.WriteString(`<col min="`)
sw.cols.WriteString(strconv.Itoa(min)) sw.cols.WriteString(strconv.Itoa(minVal))
sw.cols.WriteString(`" max="`) sw.cols.WriteString(`" max="`)
sw.cols.WriteString(strconv.Itoa(max)) sw.cols.WriteString(strconv.Itoa(maxVal))
sw.cols.WriteString(`" width="`) sw.cols.WriteString(`" width="`)
sw.cols.WriteString(strconv.FormatFloat(width, 'f', -1, 64)) sw.cols.WriteString(strconv.FormatFloat(width, 'f', -1, 64))
sw.cols.WriteString(`" customWidth="1"/>`) sw.cols.WriteString(`" customWidth="1"/>`)
@ -482,16 +484,16 @@ func (sw *StreamWriter) SetPanes(panes *Panes) error {
// MergeCell provides a function to merge cells by a given range reference for // MergeCell provides a function to merge cells by a given range reference for
// the StreamWriter. Don't create a merged cell that overlaps with another // the StreamWriter. Don't create a merged cell that overlaps with another
// existing merged cell. // existing merged cell.
func (sw *StreamWriter) MergeCell(hCell, vCell string) error { func (sw *StreamWriter) MergeCell(topLeftCell, bottomRightCell string) error {
_, err := cellRefsToCoordinates(hCell, vCell) _, err := cellRefsToCoordinates(topLeftCell, bottomRightCell)
if err != nil { if err != nil {
return err return err
} }
sw.mergeCellsCount++ sw.mergeCellsCount++
_, _ = sw.mergeCells.WriteString(`<mergeCell ref="`) _, _ = sw.mergeCells.WriteString(`<mergeCell ref="`)
_, _ = sw.mergeCells.WriteString(hCell) _, _ = sw.mergeCells.WriteString(topLeftCell)
_, _ = sw.mergeCells.WriteString(`:`) _, _ = sw.mergeCells.WriteString(`:`)
_, _ = sw.mergeCells.WriteString(vCell) _, _ = sw.mergeCells.WriteString(bottomRightCell)
_, _ = sw.mergeCells.WriteString(`"/>`) _, _ = sw.mergeCells.WriteString(`"/>`)
return nil return nil
} }
@ -525,11 +527,11 @@ func (sw *StreamWriter) setCellValFunc(c *xlsxC, val interface{}) error {
var err error var err error
switch val := val.(type) { switch val := val.(type) {
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
err = setCellIntFunc(c, val) setCellIntFunc(c, val)
case float32: case float32:
c.T, c.V = setCellFloat(float64(val), -1, 32) c.setCellFloat(float64(val), -1, 32)
case float64: case float64:
c.T, c.V = setCellFloat(val, -1, 64) c.setCellFloat(val, -1, 64)
case string: case string:
c.setCellValue(val) c.setCellValue(val)
case []byte: case []byte:
@ -552,7 +554,7 @@ func (sw *StreamWriter) setCellValFunc(c *xlsxC, val interface{}) error {
} }
// setCellIntFunc is a wrapper of SetCellInt. // setCellIntFunc is a wrapper of SetCellInt.
func setCellIntFunc(c *xlsxC, val interface{}) (err error) { func setCellIntFunc(c *xlsxC, val interface{}) {
switch val := val.(type) { switch val := val.(type) {
case int: case int:
c.T, c.V = setCellInt(val) c.T, c.V = setCellInt(val)
@ -565,18 +567,16 @@ func setCellIntFunc(c *xlsxC, val interface{}) (err error) {
case int64: case int64:
c.T, c.V = setCellInt(int(val)) c.T, c.V = setCellInt(int(val))
case uint: case uint:
c.T, c.V = setCellInt(int(val)) c.T, c.V = setCellUint(uint64(val))
case uint8: case uint8:
c.T, c.V = setCellInt(int(val)) c.T, c.V = setCellUint(uint64(val))
case uint16: case uint16:
c.T, c.V = setCellInt(int(val)) c.T, c.V = setCellUint(uint64(val))
case uint32: case uint32:
c.T, c.V = setCellInt(int(val)) c.T, c.V = setCellUint(uint64(val))
case uint64: case uint64:
c.T, c.V = setCellInt(int(val)) c.T, c.V = setCellUint(val)
default:
} }
return
} }
// writeCell constructs a cell XML and writes it to the buffer. // writeCell constructs a cell XML and writes it to the buffer.
@ -676,7 +676,7 @@ func (sw *StreamWriter) Flush() error {
sheetPath := sw.file.sheetMap[sw.Sheet] sheetPath := sw.file.sheetMap[sw.Sheet]
sw.file.Sheet.Delete(sheetPath) sw.file.Sheet.Delete(sheetPath)
delete(sw.file.checked, sheetPath) sw.file.checked.Delete(sheetPath)
sw.file.Pkg.Delete(sheetPath) sw.file.Pkg.Delete(sheetPath)
return nil return nil

View File

@ -3,6 +3,8 @@ package excelize
import ( import (
"encoding/xml" "encoding/xml"
"fmt" "fmt"
"io"
"math"
"math/rand" "math/rand"
"os" "os"
"path/filepath" "path/filepath"
@ -73,7 +75,9 @@ func TestStreamWriter(t *testing.T) {
})) }))
assert.NoError(t, streamWriter.SetRow("A6", []interface{}{time.Now()})) assert.NoError(t, streamWriter.SetRow("A6", []interface{}{time.Now()}))
assert.NoError(t, streamWriter.SetRow("A7", nil, RowOpts{Height: 20, Hidden: true, StyleID: styleID})) assert.NoError(t, streamWriter.SetRow("A7", nil, RowOpts{Height: 20, Hidden: true, StyleID: styleID}))
assert.EqualError(t, streamWriter.SetRow("A8", nil, RowOpts{Height: MaxRowHeight + 1}), ErrMaxRowHeight.Error()) assert.Equal(t, ErrMaxRowHeight, streamWriter.SetRow("A8", nil, RowOpts{Height: MaxRowHeight + 1}))
assert.NoError(t, streamWriter.SetRow("A9", []interface{}{math.NaN(), math.Inf(0), math.Inf(-1)}))
for rowID := 10; rowID <= 51200; rowID++ { for rowID := 10; rowID <= 51200; rowID++ {
row := make([]interface{}, 50) row := make([]interface{}, 50)
@ -144,7 +148,7 @@ func TestStreamWriter(t *testing.T) {
cells += len(row) cells += len(row)
} }
assert.NoError(t, rows.Close()) assert.NoError(t, rows.Close())
assert.Equal(t, 2559559, cells) assert.Equal(t, 2559562, cells)
// Save spreadsheet with password. // Save spreadsheet with password.
assert.NoError(t, file.SaveAs(filepath.Join("test", "EncryptionTestStreamWriter.xlsx"), Options{Password: "password"})) assert.NoError(t, file.SaveAs(filepath.Join("test", "EncryptionTestStreamWriter.xlsx"), Options{Password: "password"}))
assert.NoError(t, file.Close()) assert.NoError(t, file.Close())
@ -158,11 +162,11 @@ func TestStreamSetColWidth(t *testing.T) {
streamWriter, err := file.NewStreamWriter("Sheet1") streamWriter, err := file.NewStreamWriter("Sheet1")
assert.NoError(t, err) assert.NoError(t, err)
assert.NoError(t, streamWriter.SetColWidth(3, 2, 20)) assert.NoError(t, streamWriter.SetColWidth(3, 2, 20))
assert.ErrorIs(t, streamWriter.SetColWidth(0, 3, 20), ErrColumnNumber) assert.Equal(t, ErrColumnNumber, streamWriter.SetColWidth(0, 3, 20))
assert.ErrorIs(t, streamWriter.SetColWidth(MaxColumns+1, 3, 20), ErrColumnNumber) assert.Equal(t, ErrColumnNumber, streamWriter.SetColWidth(MaxColumns+1, 3, 20))
assert.EqualError(t, streamWriter.SetColWidth(1, 3, MaxColumnWidth+1), ErrColumnWidth.Error()) assert.Equal(t, ErrColumnWidth, streamWriter.SetColWidth(1, 3, MaxColumnWidth+1))
assert.NoError(t, streamWriter.SetRow("A1", []interface{}{"A", "B", "C"})) assert.NoError(t, streamWriter.SetRow("A1", []interface{}{"A", "B", "C"}))
assert.ErrorIs(t, streamWriter.SetColWidth(2, 3, 20), ErrStreamSetColWidth) assert.Equal(t, ErrStreamSetColWidth, streamWriter.SetColWidth(2, 3, 20))
} }
func TestStreamSetPanes(t *testing.T) { func TestStreamSetPanes(t *testing.T) {
@ -173,7 +177,7 @@ func TestStreamSetPanes(t *testing.T) {
YSplit: 0, YSplit: 0,
TopLeftCell: "B1", TopLeftCell: "B1",
ActivePane: "topRight", ActivePane: "topRight",
Panes: []PaneOptions{ Selection: []Selection{
{SQRef: "K16", ActiveCell: "K16", Pane: "topRight"}, {SQRef: "K16", ActiveCell: "K16", Pane: "topRight"},
}, },
} }
@ -183,9 +187,9 @@ func TestStreamSetPanes(t *testing.T) {
streamWriter, err := file.NewStreamWriter("Sheet1") streamWriter, err := file.NewStreamWriter("Sheet1")
assert.NoError(t, err) assert.NoError(t, err)
assert.NoError(t, streamWriter.SetPanes(paneOpts)) assert.NoError(t, streamWriter.SetPanes(paneOpts))
assert.EqualError(t, streamWriter.SetPanes(nil), ErrParameterInvalid.Error()) assert.Equal(t, ErrParameterInvalid, streamWriter.SetPanes(nil))
assert.NoError(t, streamWriter.SetRow("A1", []interface{}{"A", "B", "C"})) assert.NoError(t, streamWriter.SetRow("A1", []interface{}{"A", "B", "C"}))
assert.ErrorIs(t, streamWriter.SetPanes(paneOpts), ErrStreamSetPanes) assert.Equal(t, ErrStreamSetPanes, streamWriter.SetPanes(paneOpts))
} }
func TestStreamTable(t *testing.T) { func TestStreamTable(t *testing.T) {
@ -220,10 +224,12 @@ func TestStreamTable(t *testing.T) {
assert.NoError(t, streamWriter.AddTable(&Table{Range: "A1:C1"})) assert.NoError(t, streamWriter.AddTable(&Table{Range: "A1:C1"}))
// Test add table with illegal cell reference // Test add table with illegal cell reference
assert.EqualError(t, streamWriter.AddTable(&Table{Range: "A:B1"}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), streamWriter.AddTable(&Table{Range: "A:B1"}))
assert.EqualError(t, streamWriter.AddTable(&Table{Range: "A1:B"}), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")).Error()) assert.Equal(t, newCellNameToCoordinatesError("B", newInvalidCellNameError("B")), streamWriter.AddTable(&Table{Range: "A1:B"}))
// Test add table with invalid table name // Test add table with invalid table name
assert.EqualError(t, streamWriter.AddTable(&Table{Range: "A:B1", Name: "1Table"}), newInvalidTableNameError("1Table").Error()) assert.Equal(t, newInvalidNameError("1Table"), streamWriter.AddTable(&Table{Range: "A:B1", Name: "1Table"}))
// Test add table with row number exceeds maximum limit
assert.Equal(t, ErrMaxRows, streamWriter.AddTable(&Table{Range: "A1048576:C1048576"}))
// Test add table with unsupported charset content types // Test add table with unsupported charset content types
file.ContentTypes = nil file.ContentTypes = nil
file.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset) file.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
@ -239,7 +245,7 @@ func TestStreamMergeCells(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.NoError(t, streamWriter.MergeCell("A1", "D1")) assert.NoError(t, streamWriter.MergeCell("A1", "D1"))
// Test merge cells with illegal cell reference // Test merge cells with illegal cell reference
assert.EqualError(t, streamWriter.MergeCell("A", "D1"), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), streamWriter.MergeCell("A", "D1"))
assert.NoError(t, streamWriter.Flush()) assert.NoError(t, streamWriter.Flush())
// Save spreadsheet by the given path // Save spreadsheet by the given path
assert.NoError(t, file.SaveAs(filepath.Join("test", "TestStreamMergeCells.xlsx"))) assert.NoError(t, file.SaveAs(filepath.Join("test", "TestStreamMergeCells.xlsx")))
@ -270,7 +276,7 @@ func TestNewStreamWriter(t *testing.T) {
assert.EqualError(t, err, "sheet SheetN does not exist") assert.EqualError(t, err, "sheet SheetN does not exist")
// Test new stream write with invalid sheet name // Test new stream write with invalid sheet name
_, err = file.NewStreamWriter("Sheet:1") _, err = file.NewStreamWriter("Sheet:1")
assert.EqualError(t, err, ErrSheetNameInvalid.Error()) assert.Equal(t, ErrSheetNameInvalid, err)
} }
func TestStreamMarshalAttrs(t *testing.T) { func TestStreamMarshalAttrs(t *testing.T) {
@ -288,10 +294,10 @@ func TestStreamSetRow(t *testing.T) {
}() }()
streamWriter, err := file.NewStreamWriter("Sheet1") streamWriter, err := file.NewStreamWriter("Sheet1")
assert.NoError(t, err) assert.NoError(t, err)
assert.EqualError(t, streamWriter.SetRow("A", []interface{}{}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), streamWriter.SetRow("A", []interface{}{}))
// Test set row with non-ascending row number // Test set row with non-ascending row number
assert.NoError(t, streamWriter.SetRow("A1", []interface{}{})) assert.NoError(t, streamWriter.SetRow("A1", []interface{}{}))
assert.EqualError(t, streamWriter.SetRow("A1", []interface{}{}), newStreamSetRowError(1).Error()) assert.Equal(t, newStreamSetRowError(1), streamWriter.SetRow("A1", []interface{}{}))
// Test set row with unsupported charset workbook // Test set row with unsupported charset workbook
file.WorkBook = nil file.WorkBook = nil
file.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset) file.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset)
@ -332,16 +338,13 @@ func TestStreamSetRowWithStyle(t *testing.T) {
Cell{StyleID: blueStyleID, Value: "value3"}, Cell{StyleID: blueStyleID, Value: "value3"},
&Cell{StyleID: blueStyleID, Value: "value3"}, &Cell{StyleID: blueStyleID, Value: "value3"},
}, RowOpts{StyleID: grayStyleID})) }, RowOpts{StyleID: grayStyleID}))
err = streamWriter.Flush() assert.NoError(t, streamWriter.Flush())
assert.NoError(t, err)
ws, err := file.workSheetReader("Sheet1") ws, err := file.workSheetReader("Sheet1")
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, grayStyleID, ws.SheetData.Row[0].C[0].S) for colIdx, expected := range []int{grayStyleID, zeroStyleID, zeroStyleID, blueStyleID, blueStyleID} {
assert.Equal(t, zeroStyleID, ws.SheetData.Row[0].C[1].S) assert.Equal(t, expected, ws.SheetData.Row[0].C[colIdx].S)
assert.Equal(t, zeroStyleID, ws.SheetData.Row[0].C[2].S) }
assert.Equal(t, blueStyleID, ws.SheetData.Row[0].C[3].S)
assert.Equal(t, blueStyleID, ws.SheetData.Row[0].C[4].S)
} }
func TestStreamSetCellValFunc(t *testing.T) { func TestStreamSetCellValFunc(t *testing.T) {
@ -352,25 +355,29 @@ func TestStreamSetCellValFunc(t *testing.T) {
sw, err := f.NewStreamWriter("Sheet1") sw, err := f.NewStreamWriter("Sheet1")
assert.NoError(t, err) assert.NoError(t, err)
c := &xlsxC{} c := &xlsxC{}
assert.NoError(t, sw.setCellValFunc(c, 128)) for _, val := range []interface{}{
assert.NoError(t, sw.setCellValFunc(c, int8(-128))) 128,
assert.NoError(t, sw.setCellValFunc(c, int16(-32768))) int8(-128),
assert.NoError(t, sw.setCellValFunc(c, int32(-2147483648))) int16(-32768),
assert.NoError(t, sw.setCellValFunc(c, int64(-9223372036854775808))) int32(-2147483648),
assert.NoError(t, sw.setCellValFunc(c, uint(128))) int64(-9223372036854775808),
assert.NoError(t, sw.setCellValFunc(c, uint8(255))) uint(128),
assert.NoError(t, sw.setCellValFunc(c, uint16(65535))) uint8(255),
assert.NoError(t, sw.setCellValFunc(c, uint32(4294967295))) uint16(65535),
assert.NoError(t, sw.setCellValFunc(c, uint64(18446744073709551615))) uint32(4294967295),
assert.NoError(t, sw.setCellValFunc(c, float32(100.1588))) uint64(18446744073709551615),
assert.NoError(t, sw.setCellValFunc(c, 100.1588)) float32(100.1588),
assert.NoError(t, sw.setCellValFunc(c, " Hello")) 100.1588,
assert.NoError(t, sw.setCellValFunc(c, []byte(" Hello"))) " Hello",
assert.NoError(t, sw.setCellValFunc(c, time.Now().UTC())) []byte(" Hello"),
assert.NoError(t, sw.setCellValFunc(c, time.Duration(1e13))) time.Now().UTC(),
assert.NoError(t, sw.setCellValFunc(c, true)) time.Duration(1e13),
assert.NoError(t, sw.setCellValFunc(c, nil)) true,
assert.NoError(t, sw.setCellValFunc(c, complex64(5+10i))) nil,
complex64(5 + 10i),
} {
assert.NoError(t, sw.setCellValFunc(c, val))
}
} }
func TestStreamWriterOutlineLevel(t *testing.T) { func TestStreamWriterOutlineLevel(t *testing.T) {
@ -389,14 +396,61 @@ func TestStreamWriterOutlineLevel(t *testing.T) {
file, err = OpenFile(filepath.Join("test", "TestStreamWriterSetRowOutlineLevel.xlsx")) file, err = OpenFile(filepath.Join("test", "TestStreamWriterSetRowOutlineLevel.xlsx"))
assert.NoError(t, err) assert.NoError(t, err)
level, err := file.GetRowOutlineLevel("Sheet1", 1) for rowIdx, expected := range []uint8{1, 7, 0} {
assert.NoError(t, err) level, err := file.GetRowOutlineLevel("Sheet1", rowIdx+1)
assert.Equal(t, uint8(1), level) assert.NoError(t, err)
level, err = file.GetRowOutlineLevel("Sheet1", 2) assert.Equal(t, expected, level)
assert.NoError(t, err) }
assert.Equal(t, uint8(7), level)
level, err = file.GetRowOutlineLevel("Sheet1", 3)
assert.NoError(t, err)
assert.Equal(t, uint8(0), level)
assert.NoError(t, file.Close()) assert.NoError(t, file.Close())
} }
func TestStreamWriterReader(t *testing.T) {
var (
err error
sw = StreamWriter{
rawData: bufferedWriter{},
}
)
sw.rawData.tmp, err = os.CreateTemp(os.TempDir(), "excelize-")
assert.NoError(t, err)
assert.NoError(t, sw.rawData.tmp.Close())
// Test reader stat a closed temp file
_, err = sw.rawData.Reader()
assert.Error(t, err)
_, err = sw.getRowValues(1, 1, 1)
assert.Error(t, err)
os.Remove(sw.rawData.tmp.Name())
sw = StreamWriter{
file: NewFile(),
rawData: bufferedWriter{},
}
// Test getRowValues without expected row
sw.rawData.buf.WriteString("<worksheet><row r=\"1\"><c r=\"B1\"></c></row><worksheet/>")
_, err = sw.getRowValues(1, 1, 1)
assert.NoError(t, err)
sw.rawData.buf.Reset()
// Test getRowValues with illegal cell reference
sw.rawData.buf.WriteString("<worksheet><row r=\"1\"><c r=\"A\"></c></row><worksheet/>")
_, err = sw.getRowValues(1, 1, 1)
assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), err)
sw.rawData.buf.Reset()
// Test getRowValues with invalid c element characters
sw.rawData.buf.WriteString("<worksheet><row r=\"1\"><c></row><worksheet/>")
_, err = sw.getRowValues(1, 1, 1)
assert.EqualError(t, err, "XML syntax error on line 1: element <c> closed by </row>")
sw.rawData.buf.Reset()
}
func TestStreamWriterGetRowElement(t *testing.T) {
// Test get row element without r attribute
dec := xml.NewDecoder(strings.NewReader("<row ht=\"0\" />"))
for {
token, err := dec.Token()
if err == io.EOF {
break
}
_, ok := getRowElement(token, 0)
assert.False(t, ok)
}
}

2693
styles.go

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,7 @@
package excelize package excelize
import ( import (
"fmt"
"math" "math"
"path/filepath" "path/filepath"
"strings" "strings"
@ -171,18 +172,20 @@ func TestSetConditionalFormat(t *testing.T) {
// Test creating a conditional format with a solid color data bar style // Test creating a conditional format with a solid color data bar style
f := NewFile() f := NewFile()
condFmts := []ConditionalFormatOptions{ condFmts := []ConditionalFormatOptions{
{Type: "data_bar", BarColor: "#A9D08E", BarSolid: true, Format: 0, Criteria: "=", MinType: "min", MaxType: "max"}, {Type: "data_bar", BarColor: "#A9D08E", BarSolid: true, Format: intPtr(0), Criteria: "=", MinType: "min", MaxType: "max"},
} }
for _, ref := range []string{"A1:A2", "B1:B2"} { for _, ref := range []string{"A1:A2", "B1:B2"} {
assert.NoError(t, f.SetConditionalFormat("Sheet1", ref, condFmts)) assert.NoError(t, f.SetConditionalFormat("Sheet1", ref, condFmts))
} }
f = NewFile() f = NewFile()
// Test creating a conditional format without cell reference
assert.Equal(t, ErrParameterRequired, f.SetConditionalFormat("Sheet1", "", nil))
// Test creating a conditional format with invalid cell reference
assert.Equal(t, ErrParameterInvalid, f.SetConditionalFormat("Sheet1", "A1:A2:A3", nil))
// Test creating a conditional format with existing extension lists // Test creating a conditional format with existing extension lists
ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml") ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
assert.True(t, ok) assert.True(t, ok)
ws.(*xlsxWorksheet).ExtLst = &xlsxExtLst{Ext: ` ws.(*xlsxWorksheet).ExtLst = &xlsxExtLst{Ext: fmt.Sprintf(`<ext uri="%s"><x14:slicerList /></ext><ext uri="%s"><x14:sparklineGroups /></ext>`, ExtURISlicerListX14, ExtURISparklineGroups)}
<ext uri="{A8765BA9-456A-4dab-B4F3-ACF838C121DE}"><x14:slicerList /></ext>
<ext uri="{05C60535-1F16-4fd2-B633-F4F36F0B64E0}"><x14:sparklineGroups /></ext>`}
assert.NoError(t, f.SetConditionalFormat("Sheet1", "A1:A2", []ConditionalFormatOptions{{Type: "data_bar", Criteria: "=", MinType: "min", MaxType: "max", BarBorderColor: "#0000FF", BarColor: "#638EC6", BarSolid: true}})) assert.NoError(t, f.SetConditionalFormat("Sheet1", "A1:A2", []ConditionalFormatOptions{{Type: "data_bar", Criteria: "=", MinType: "min", MaxType: "max", BarBorderColor: "#0000FF", BarColor: "#638EC6", BarSolid: true}}))
f = NewFile() f = NewFile()
// Test creating a conditional format with invalid extension list characters // Test creating a conditional format with invalid extension list characters
@ -191,39 +194,113 @@ func TestSetConditionalFormat(t *testing.T) {
ws.(*xlsxWorksheet).ExtLst = &xlsxExtLst{Ext: "<ext><x14:conditionalFormattings></x14:conditionalFormatting></x14:conditionalFormattings></ext>"} ws.(*xlsxWorksheet).ExtLst = &xlsxExtLst{Ext: "<ext><x14:conditionalFormattings></x14:conditionalFormatting></x14:conditionalFormattings></ext>"}
assert.EqualError(t, f.SetConditionalFormat("Sheet1", "A1:A2", condFmts), "XML syntax error on line 1: element <conditionalFormattings> closed by </conditionalFormatting>") assert.EqualError(t, f.SetConditionalFormat("Sheet1", "A1:A2", condFmts), "XML syntax error on line 1: element <conditionalFormattings> closed by </conditionalFormatting>")
// Test creating a conditional format with invalid icon set style // Test creating a conditional format with invalid icon set style
assert.EqualError(t, f.SetConditionalFormat("Sheet1", "A1:A2", []ConditionalFormatOptions{{Type: "icon_set", IconStyle: "unknown"}}), ErrParameterInvalid.Error()) assert.Equal(t, ErrParameterInvalid, f.SetConditionalFormat("Sheet1", "A1:A2", []ConditionalFormatOptions{{Type: "icon_set", IconStyle: "unknown"}}))
// Test unsupported conditional formatting rule types
assert.Equal(t, ErrParameterInvalid, f.SetConditionalFormat("Sheet1", "A1", []ConditionalFormatOptions{{Type: "unsupported"}}))
t.Run("multi_conditional_formatting_rules_priority", func(t *testing.T) {
f := NewFile()
var condFmts []ConditionalFormatOptions
for _, color := range []string{
"#264B96", // Blue
"#F9A73E", // Yellow
"#006F3C", // Green
} {
condFmts = append(condFmts, ConditionalFormatOptions{
Type: "data_bar",
Criteria: "=",
MinType: "num",
MaxType: "num",
MinValue: "0",
MaxValue: "5",
BarColor: color,
BarSolid: true,
})
}
assert.NoError(t, f.SetConditionalFormat("Sheet1", "A1:A5", condFmts))
assert.NoError(t, f.SetConditionalFormat("Sheet1", "B1:B5", condFmts))
for r := 1; r <= 20; r++ {
cell, err := CoordinatesToCellName(1, r)
assert.NoError(t, err)
assert.NoError(t, f.SetCellValue("Sheet1", cell, r))
cell, err = CoordinatesToCellName(2, r)
assert.NoError(t, err)
assert.NoError(t, f.SetCellValue("Sheet1", cell, r))
}
ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
assert.True(t, ok)
var priorities []int
expected := []int{1, 2, 3, 4, 5, 6}
for _, condFmt := range ws.(*xlsxWorksheet).ConditionalFormatting {
for _, rule := range condFmt.CfRule {
priorities = append(priorities, rule.Priority)
}
}
assert.Equal(t, expected, priorities)
assert.NoError(t, f.Close())
})
} }
func TestGetConditionalFormats(t *testing.T) { func TestGetConditionalFormats(t *testing.T) {
for _, format := range [][]ConditionalFormatOptions{ for _, format := range [][]ConditionalFormatOptions{
{{Type: "cell", Format: 1, Criteria: "greater than", Value: "6"}}, {{Type: "cell", Format: intPtr(1), Criteria: "greater than", Value: "6"}},
{{Type: "cell", Format: 1, Criteria: "between", MinValue: "6", MaxValue: "8"}}, {{Type: "cell", Format: intPtr(1), Criteria: "between", MinValue: "6", MaxValue: "8"}},
{{Type: "top", Format: 1, Criteria: "=", Value: "6"}}, {{Type: "time_period", Format: intPtr(1), Criteria: "yesterday"}},
{{Type: "bottom", Format: 1, Criteria: "=", Value: "6"}}, {{Type: "time_period", Format: intPtr(1), Criteria: "today"}},
{{Type: "average", AboveAverage: true, Format: 1, Criteria: "="}}, {{Type: "time_period", Format: intPtr(1), Criteria: "tomorrow"}},
{{Type: "duplicate", Format: 1, Criteria: "="}}, {{Type: "time_period", Format: intPtr(1), Criteria: "last 7 days"}},
{{Type: "unique", Format: 1, Criteria: "="}}, {{Type: "time_period", Format: intPtr(1), Criteria: "last week"}},
{{Type: "time_period", Format: intPtr(1), Criteria: "this week"}},
{{Type: "time_period", Format: intPtr(1), Criteria: "continue week"}},
{{Type: "time_period", Format: intPtr(1), Criteria: "last month"}},
{{Type: "time_period", Format: intPtr(1), Criteria: "this month"}},
{{Type: "time_period", Format: intPtr(1), Criteria: "continue month"}},
{{Type: "text", Format: intPtr(1), Criteria: "containing", Value: "~!@#$%^&*()_+{}|:<>?\"';"}},
{{Type: "text", Format: intPtr(1), Criteria: "not containing", Value: "text"}},
{{Type: "text", Format: intPtr(1), Criteria: "begins with", Value: "prefix"}},
{{Type: "text", Format: intPtr(1), Criteria: "ends with", Value: "suffix"}},
{{Type: "top", Format: intPtr(1), Criteria: "=", Value: "6"}},
{{Type: "bottom", Format: intPtr(1), Criteria: "=", Value: "6"}},
{{Type: "average", AboveAverage: true, Format: intPtr(1), Criteria: "="}},
{{Type: "duplicate", Format: intPtr(1), Criteria: "="}},
{{Type: "unique", Format: intPtr(1), Criteria: "="}},
{{Type: "3_color_scale", Criteria: "=", MinType: "num", MidType: "num", MaxType: "num", MinValue: "-10", MidValue: "50", MaxValue: "10", MinColor: "#FF0000", MidColor: "#00FF00", MaxColor: "#0000FF"}}, {{Type: "3_color_scale", Criteria: "=", MinType: "num", MidType: "num", MaxType: "num", MinValue: "-10", MidValue: "50", MaxValue: "10", MinColor: "#FF0000", MidColor: "#00FF00", MaxColor: "#0000FF"}},
{{Type: "2_color_scale", Criteria: "=", MinType: "num", MaxType: "num", MinColor: "#FF0000", MaxColor: "#0000FF"}}, {{Type: "2_color_scale", Criteria: "=", MinType: "num", MaxType: "num", MinColor: "#FF0000", MaxColor: "#0000FF"}},
{{Type: "data_bar", Criteria: "=", MinType: "num", MaxType: "num", MinValue: "-10", MaxValue: "10", BarBorderColor: "#0000FF", BarColor: "#638EC6", BarOnly: true, BarSolid: true, StopIfTrue: true}}, {{Type: "data_bar", Criteria: "=", MinType: "num", MaxType: "num", MinValue: "-10", MaxValue: "10", BarBorderColor: "#0000FF", BarColor: "#638EC6", BarOnly: true, BarSolid: true, StopIfTrue: true}},
{{Type: "data_bar", Criteria: "=", MinType: "min", MaxType: "max", BarBorderColor: "#0000FF", BarColor: "#638EC6", BarDirection: "rightToLeft", BarOnly: true, BarSolid: true, StopIfTrue: true}}, {{Type: "data_bar", Criteria: "=", MinType: "min", MaxType: "max", BarBorderColor: "#0000FF", BarColor: "#638EC6", BarDirection: "rightToLeft", BarOnly: true, BarSolid: true, StopIfTrue: true}},
{{Type: "formula", Format: 1, Criteria: "="}}, {{Type: "formula", Format: intPtr(1), Criteria: "="}},
{{Type: "blanks", Format: intPtr(1)}},
{{Type: "no_blanks", Format: intPtr(1)}},
{{Type: "errors", Format: intPtr(1)}},
{{Type: "no_errors", Format: intPtr(1)}},
{{Type: "icon_set", IconStyle: "3Arrows", ReverseIcons: true, IconsOnly: true}}, {{Type: "icon_set", IconStyle: "3Arrows", ReverseIcons: true, IconsOnly: true}},
} { } {
f := NewFile() f := NewFile()
err := f.SetConditionalFormat("Sheet1", "A1:A2", format) err := f.SetConditionalFormat("Sheet1", "A2:A1,B:B,2:2", format)
assert.NoError(t, err) assert.NoError(t, err)
opts, err := f.GetConditionalFormats("Sheet1") opts, err := f.GetConditionalFormats("Sheet1")
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, format, opts["A1:A2"]) assert.Equal(t, format, opts["A2:A1 B1:B1048576 A2:XFD2"])
} }
// Test get conditional formats on no exists worksheet // Test get multiple conditional formats
f := NewFile() f := NewFile()
_, err := f.GetConditionalFormats("SheetN") expected := []ConditionalFormatOptions{
{Type: "data_bar", Criteria: "=", MinType: "num", MaxType: "num", MinValue: "-10", MaxValue: "10", BarBorderColor: "#0000FF", BarColor: "#638EC6", BarOnly: true, BarSolid: true, StopIfTrue: true},
{Type: "data_bar", Criteria: "=", MinType: "min", MaxType: "max", BarBorderColor: "#0000FF", BarColor: "#638EC6", BarDirection: "rightToLeft", BarOnly: true, BarSolid: false, StopIfTrue: true},
}
err := f.SetConditionalFormat("Sheet1", "A1:A2", expected)
assert.NoError(t, err)
opts, err := f.GetConditionalFormats("Sheet1")
assert.NoError(t, err)
assert.Equal(t, expected, opts["A1:A2"])
// Test get conditional formats on no exists worksheet
f = NewFile()
_, err = f.GetConditionalFormats("SheetN")
assert.EqualError(t, err, "sheet SheetN does not exist") assert.EqualError(t, err, "sheet SheetN does not exist")
// Test get conditional formats with invalid sheet name // Test get conditional formats with invalid sheet name
_, err = f.GetConditionalFormats("Sheet:1") _, err = f.GetConditionalFormats("Sheet:1")
assert.EqualError(t, err, ErrSheetNameInvalid.Error()) assert.Equal(t, ErrSheetNameInvalid, err)
} }
func TestUnsetConditionalFormat(t *testing.T) { func TestUnsetConditionalFormat(t *testing.T) {
@ -232,12 +309,12 @@ func TestUnsetConditionalFormat(t *testing.T) {
assert.NoError(t, f.UnsetConditionalFormat("Sheet1", "A1:A10")) assert.NoError(t, f.UnsetConditionalFormat("Sheet1", "A1:A10"))
format, err := f.NewConditionalStyle(&Style{Font: &Font{Color: "9A0511"}, Fill: Fill{Type: "pattern", Color: []string{"FEC7CE"}, Pattern: 1}}) format, err := f.NewConditionalStyle(&Style{Font: &Font{Color: "9A0511"}, Fill: Fill{Type: "pattern", Color: []string{"FEC7CE"}, Pattern: 1}})
assert.NoError(t, err) assert.NoError(t, err)
assert.NoError(t, f.SetConditionalFormat("Sheet1", "A1:A10", []ConditionalFormatOptions{{Type: "cell", Criteria: ">", Format: format, Value: "6"}})) assert.NoError(t, f.SetConditionalFormat("Sheet1", "A1:A10", []ConditionalFormatOptions{{Type: "cell", Criteria: ">", Format: &format, Value: "6"}}))
assert.NoError(t, f.UnsetConditionalFormat("Sheet1", "A1:A10")) assert.NoError(t, f.UnsetConditionalFormat("Sheet1", "A1:A10"))
// Test unset conditional format on not exists worksheet // Test unset conditional format on not exists worksheet
assert.EqualError(t, f.UnsetConditionalFormat("SheetN", "A1:A10"), "sheet SheetN does not exist") assert.EqualError(t, f.UnsetConditionalFormat("SheetN", "A1:A10"), "sheet SheetN does not exist")
// Test unset conditional format with invalid sheet name // Test unset conditional format with invalid sheet name
assert.EqualError(t, f.UnsetConditionalFormat("Sheet:1", "A1:A10"), ErrSheetNameInvalid.Error()) assert.Equal(t, ErrSheetNameInvalid, f.UnsetConditionalFormat("Sheet:1", "A1:A10"))
// Save spreadsheet by the given path // Save spreadsheet by the given path
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestUnsetConditionalFormat.xlsx"))) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestUnsetConditionalFormat.xlsx")))
} }
@ -264,13 +341,22 @@ func TestNewStyle(t *testing.T) {
_, err = f.NewStyle(nil) _, err = f.NewStyle(nil)
assert.NoError(t, err) assert.NoError(t, err)
// Test gradient fills
f = NewFile()
styleID1, err := f.NewStyle(&Style{Fill: Fill{Type: "gradient", Color: []string{"FFFFFF", "4E71BE"}, Shading: 1, Pattern: 1}})
assert.NoError(t, err)
styleID2, err := f.NewStyle(&Style{Fill: Fill{Type: "gradient", Color: []string{"FF0000", "4E71BE"}, Shading: 1, Pattern: 1}})
assert.NoError(t, err)
assert.NotEqual(t, styleID1, styleID2)
var exp string var exp string
f = NewFile()
_, err = f.NewStyle(&Style{CustomNumFmt: &exp}) _, err = f.NewStyle(&Style{CustomNumFmt: &exp})
assert.EqualError(t, err, ErrCustomNumFmt.Error()) assert.Equal(t, ErrCustomNumFmt, err)
_, err = f.NewStyle(&Style{Font: &Font{Family: strings.Repeat("s", MaxFontFamilyLength+1)}}) _, err = f.NewStyle(&Style{Font: &Font{Family: strings.Repeat("s", MaxFontFamilyLength+1)}})
assert.EqualError(t, err, ErrFontLength.Error()) assert.Equal(t, ErrFontLength, err)
_, err = f.NewStyle(&Style{Font: &Font{Size: MaxFontSize + 1}}) _, err = f.NewStyle(&Style{Font: &Font{Size: MaxFontSize + 1}})
assert.EqualError(t, err, ErrFontSize.Error()) assert.Equal(t, ErrFontSize, err)
// Test create numeric custom style // Test create numeric custom style
numFmt := "####;####" numFmt := "####;####"
@ -279,7 +365,7 @@ func TestNewStyle(t *testing.T) {
CustomNumFmt: &numFmt, CustomNumFmt: &numFmt,
}) })
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 2, styleID) assert.Equal(t, 1, styleID)
assert.NotNil(t, f.Styles) assert.NotNil(t, f.Styles)
assert.NotNil(t, f.Styles.CellXfs) assert.NotNil(t, f.Styles.CellXfs)
@ -291,12 +377,10 @@ func TestNewStyle(t *testing.T) {
// Test create currency custom style // Test create currency custom style
f.Styles.NumFmts = nil f.Styles.NumFmts = nil
styleID, err = f.NewStyle(&Style{ styleID, err = f.NewStyle(&Style{
Lang: "ko-kr",
NumFmt: 32, // must not be in currencyNumFmt NumFmt: 32, // must not be in currencyNumFmt
}) })
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 3, styleID) assert.Equal(t, 2, styleID)
assert.NotNil(t, f.Styles) assert.NotNil(t, f.Styles)
assert.NotNil(t, f.Styles.CellXfs) assert.NotNil(t, f.Styles.CellXfs)
@ -330,14 +414,14 @@ func TestNewStyle(t *testing.T) {
f = NewFile() f = NewFile()
f.Styles.NumFmts = nil f.Styles.NumFmts = nil
f.Styles.CellXfs.Xf = nil f.Styles.CellXfs.Xf = nil
style4, err := f.NewStyle(&Style{NumFmt: 160, Lang: "unknown"}) style4, err := f.NewStyle(&Style{NumFmt: 160})
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 0, style4) assert.Equal(t, 0, style4)
f = NewFile() f = NewFile()
f.Styles.NumFmts = nil f.Styles.NumFmts = nil
f.Styles.CellXfs.Xf = nil f.Styles.CellXfs.Xf = nil
style5, err := f.NewStyle(&Style{NumFmt: 160, Lang: "zh-cn"}) style5, err := f.NewStyle(&Style{NumFmt: 160})
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, 0, style5) assert.Equal(t, 0, style5)
@ -355,13 +439,58 @@ func TestNewStyle(t *testing.T) {
assert.Equal(t, ErrCellStyles, err) assert.Equal(t, ErrCellStyles, err)
} }
func TestNewConditionalStyle(t *testing.T) { func TestConditionalStyle(t *testing.T) {
f := NewFile() f := NewFile()
expected := &Style{Protection: &Protection{Hidden: true, Locked: true}}
idx, err := f.NewConditionalStyle(expected)
assert.NoError(t, err)
style, err := f.GetConditionalStyle(idx)
assert.NoError(t, err)
assert.Equal(t, expected, style)
_, err = f.NewConditionalStyle(&Style{DecimalPlaces: intPtr(4), NumFmt: 165, NegRed: true})
assert.NoError(t, err)
_, err = f.NewConditionalStyle(&Style{DecimalPlaces: intPtr(-1)})
assert.NoError(t, err)
expected = &Style{NumFmt: 1}
idx, err = f.NewConditionalStyle(expected)
assert.NoError(t, err)
style, err = f.GetConditionalStyle(idx)
assert.NoError(t, err)
assert.Equal(t, expected.NumFmt, style.NumFmt)
assert.Zero(t, *style.DecimalPlaces)
_, err = f.NewConditionalStyle(&Style{NumFmt: 27})
assert.NoError(t, err)
numFmt := "general"
_, err = f.NewConditionalStyle(&Style{CustomNumFmt: &numFmt})
assert.NoError(t, err)
numFmt1 := "0.00"
_, err = f.NewConditionalStyle(&Style{CustomNumFmt: &numFmt1})
assert.NoError(t, err)
// Test create conditional style with unsupported charset style sheet // Test create conditional style with unsupported charset style sheet
f.Styles = nil f.Styles = nil
f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset) f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset)
_, err := f.NewConditionalStyle(&Style{Font: &Font{Color: "9A0511"}, Fill: Fill{Type: "pattern", Color: []string{"FEC7CE"}, Pattern: 1}}) _, err = f.NewConditionalStyle(&Style{Font: &Font{Color: "9A0511"}, Fill: Fill{Type: "pattern", Color: []string{"FEC7CE"}, Pattern: 1}})
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
// Test get conditional style with invalid style index
_, err = f.GetConditionalStyle(1)
assert.Equal(t, newInvalidStyleID(1), err)
// Test get conditional style with unsupported charset style sheet
f.Styles = nil
f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset)
_, err = f.GetConditionalStyle(1)
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
f = NewFile()
// Test get conditional style with background color and empty pattern type
idx, err = f.NewConditionalStyle(&Style{Fill: Fill{Type: "pattern", Color: []string{"FEC7CE"}, Pattern: 1}})
assert.NoError(t, err)
f.Styles.Dxfs.Dxfs[0].Fill.PatternFill.PatternType = ""
f.Styles.Dxfs.Dxfs[0].Fill.PatternFill.FgColor = nil
f.Styles.Dxfs.Dxfs[0].Fill.PatternFill.BgColor = &xlsxColor{Theme: intPtr(6)}
style, err = f.GetConditionalStyle(idx)
assert.NoError(t, err)
assert.Equal(t, "pattern", style.Fill.Type)
assert.Equal(t, []string{"A5A5A5"}, style.Fill.Color)
} }
func TestGetDefaultFont(t *testing.T) { func TestGetDefaultFont(t *testing.T) {
@ -407,7 +536,7 @@ func TestThemeReader(t *testing.T) {
f.Pkg.Store(defaultXMLPathTheme, MacintoshCyrillicCharset) f.Pkg.Store(defaultXMLPathTheme, MacintoshCyrillicCharset)
theme, err := f.themeReader() theme, err := f.themeReader()
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
assert.EqualValues(t, &xlsxTheme{XMLNSa: NameSpaceDrawingML.Value, XMLNSr: SourceRelationship.Value}, theme) assert.EqualValues(t, &decodeTheme{}, theme)
} }
func TestSetCellStyle(t *testing.T) { func TestSetCellStyle(t *testing.T) {
@ -415,9 +544,9 @@ func TestSetCellStyle(t *testing.T) {
// Test set cell style on not exists worksheet // Test set cell style on not exists worksheet
assert.EqualError(t, f.SetCellStyle("SheetN", "A1", "A2", 1), "sheet SheetN does not exist") assert.EqualError(t, f.SetCellStyle("SheetN", "A1", "A2", 1), "sheet SheetN does not exist")
// Test set cell style with invalid style ID // Test set cell style with invalid style ID
assert.EqualError(t, f.SetCellStyle("Sheet1", "A1", "A2", -1), newInvalidStyleID(-1).Error()) assert.Equal(t, newInvalidStyleID(-1), f.SetCellStyle("Sheet1", "A1", "A2", -1))
// Test set cell style with not exists style ID // Test set cell style with not exists style ID
assert.EqualError(t, f.SetCellStyle("Sheet1", "A1", "A2", 10), newInvalidStyleID(10).Error()) assert.Equal(t, newInvalidStyleID(10), f.SetCellStyle("Sheet1", "A1", "A2", 10))
// Test set cell style with unsupported charset style sheet // Test set cell style with unsupported charset style sheet
f.Styles = nil f.Styles = nil
f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset) f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset)
@ -474,3 +603,161 @@ func TestGetNumFmtID(t *testing.T) {
assert.NotEqual(t, id1, id2) assert.NotEqual(t, id1, id2)
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestStyleNumFmt.xlsx"))) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestStyleNumFmt.xlsx")))
} }
func TestGetThemeColor(t *testing.T) {
assert.Empty(t, (&File{}).getThemeColor(&xlsxColor{}))
f := NewFile()
assert.Empty(t, f.getThemeColor(nil))
var theme int
assert.Equal(t, "FFFFFF", f.getThemeColor(&xlsxColor{Theme: &theme}))
assert.Equal(t, "FFFFFF", f.getThemeColor(&xlsxColor{RGB: "FFFFFF"}))
assert.Equal(t, "FF8080", f.getThemeColor(&xlsxColor{Indexed: 2, Tint: 0.5}))
assert.Empty(t, f.getThemeColor(&xlsxColor{Indexed: len(IndexedColorMapping), Tint: 0.5}))
clr := &decodeCTColor{}
assert.Nil(t, clr.colorChoice())
}
func TestGetStyle(t *testing.T) {
f := NewFile()
expected := &Style{
Border: []Border{
{Type: "left", Color: "0000FF", Style: 3},
{Type: "right", Color: "FF0000", Style: 6},
{Type: "top", Color: "00FF00", Style: 4},
{Type: "bottom", Color: "FFFF00", Style: 5},
{Type: "diagonalUp", Color: "A020F0", Style: 7},
{Type: "diagonalDown", Color: "A020F0", Style: 7},
},
Fill: Fill{Type: "gradient", Shading: 16, Color: []string{"0000FF", "00FF00"}},
Font: &Font{
Bold: true, Italic: true, Underline: "single", Family: "Arial",
Size: 8.5, Strike: true, Color: "777777", ColorIndexed: 1, ColorTint: 0.1,
},
Alignment: &Alignment{
Horizontal: "center",
Indent: 1,
JustifyLastLine: true,
ReadingOrder: 1,
RelativeIndent: 1,
ShrinkToFit: true,
TextRotation: 180,
Vertical: "center",
WrapText: true,
},
Protection: &Protection{Hidden: true, Locked: true},
NumFmt: 49,
}
styleID, err := f.NewStyle(expected)
assert.NoError(t, err)
style, err := f.GetStyle(styleID)
assert.NoError(t, err)
assert.Equal(t, expected.Border, style.Border)
assert.Equal(t, expected.Fill, style.Fill)
assert.Equal(t, expected.Font, style.Font)
assert.Equal(t, expected.Alignment, style.Alignment)
assert.Equal(t, expected.Protection, style.Protection)
assert.Equal(t, expected.NumFmt, style.NumFmt)
assert.Nil(t, style.DecimalPlaces)
expected = &Style{
Fill: Fill{Type: "pattern", Pattern: 1, Color: []string{"0000FF"}},
}
styleID, err = f.NewStyle(expected)
assert.NoError(t, err)
style, err = f.GetStyle(styleID)
assert.NoError(t, err)
assert.Equal(t, expected.Fill, style.Fill)
assert.Nil(t, style.DecimalPlaces)
expected = &Style{NumFmt: 2}
styleID, err = f.NewStyle(expected)
assert.NoError(t, err)
style, err = f.GetStyle(styleID)
assert.NoError(t, err)
assert.Equal(t, expected.NumFmt, style.NumFmt)
assert.Equal(t, 2, *style.DecimalPlaces)
expected = &Style{NumFmt: 27}
styleID, err = f.NewStyle(expected)
assert.NoError(t, err)
style, err = f.GetStyle(styleID)
assert.NoError(t, err)
assert.Equal(t, expected.NumFmt, style.NumFmt)
assert.Nil(t, style.DecimalPlaces)
expected = &Style{NumFmt: 165}
styleID, err = f.NewStyle(expected)
assert.NoError(t, err)
style, err = f.GetStyle(styleID)
assert.NoError(t, err)
assert.Equal(t, expected.NumFmt, style.NumFmt)
assert.Equal(t, 2, *style.DecimalPlaces)
decimal := 4
expected = &Style{NumFmt: 165, DecimalPlaces: &decimal, NegRed: true}
styleID, err = f.NewStyle(expected)
assert.NoError(t, err)
style, err = f.GetStyle(styleID)
assert.NoError(t, err)
assert.Equal(t, 0, style.NumFmt)
assert.Equal(t, *expected.DecimalPlaces, *style.DecimalPlaces)
assert.Equal(t, "[$$-409]#,##0.0000;[Red][$$-409]#,##0.0000", *style.CustomNumFmt)
for _, val := range [][]interface{}{
{"$#,##0", 0},
{"$#,##0.0", 1},
{"_($* #,##0_);_($* (#,##0);_($* \"-\"_);_(@_)", 0},
{"_($* #,##000_);_($* (#,##000);_($* \"-\"_);_(@_)", 0},
{"_($* #,##0.0000_);_($* (#,##0.0000);_($* \"-\"????_);_(@_)", 4},
} {
numFmtCode := val[0].(string)
expected = &Style{CustomNumFmt: &numFmtCode}
styleID, err = f.NewStyle(expected)
assert.NoError(t, err)
style, err = f.GetStyle(styleID)
assert.NoError(t, err)
assert.Equal(t, val[1].(int), *style.DecimalPlaces, numFmtCode)
}
for _, val := range []string{
";$#,##0",
";$#,##0;",
";$#,##0.0",
";$#,##0.0;",
"$#,##0;0.0",
"_($* #,##0_);;_($* \"-\"_);_(@_)",
"_($* #,##0.0_);_($* (#,##0.00);_($* \"-\"_);_(@_)",
} {
expected = &Style{CustomNumFmt: &val}
styleID, err = f.NewStyle(expected)
assert.NoError(t, err)
style, err = f.GetStyle(styleID)
assert.NoError(t, err)
assert.Nil(t, style.DecimalPlaces)
}
// Test get style with custom color index
f.Styles.Colors = &xlsxStyleColors{
IndexedColors: &xlsxIndexedColors{
RgbColor: []xlsxColor{{RGB: "FF012345"}},
},
}
assert.Equal(t, "012345", f.getThemeColor(&xlsxColor{Indexed: 0}))
f.Styles.Fonts.Font[0].U = &attrValString{}
f.Styles.CellXfs.Xf[0].FontID = intPtr(0)
style, err = f.GetStyle(styleID)
assert.NoError(t, err)
assert.Equal(t, "single", style.Font.Underline)
// Test get style with invalid style index
style, err = f.GetStyle(-1)
assert.Nil(t, style)
assert.Equal(t, err, newInvalidStyleID(-1))
// Test get style with unsupported charset style sheet
f.Styles = nil
f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset)
style, err = f.GetStyle(1)
assert.Nil(t, style)
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
}

309
table.go
View File

@ -1,4 +1,4 @@
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in // this source code is governed by a BSD-style license that can be found in
// the LICENSE file. // the LICENSE file.
// //
@ -7,20 +7,28 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming // Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of // API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.16 or later. // data. This library needs Go version 1.18 or later.
package excelize package excelize
import ( import (
"bytes"
"encoding/xml" "encoding/xml"
"fmt" "fmt"
"io"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
"unicode"
"unicode/utf8" "unicode/utf8"
) )
var (
expressionFormat = regexp.MustCompile(`"(?:[^"]|"")*"|\S+`)
conditionFormat = regexp.MustCompile(`(or|\|\|)`)
blankFormat = regexp.MustCompile("blanks|nonblanks")
matchFormat = regexp.MustCompile("[*?]")
)
// parseTableOptions provides a function to parse the format settings of the // parseTableOptions provides a function to parse the format settings of the
// table with default value. // table with default value.
func parseTableOptions(opts *Table) (*Table, error) { func parseTableOptions(opts *Table) (*Table, error) {
@ -31,7 +39,7 @@ func parseTableOptions(opts *Table) (*Table, error) {
if opts.ShowRowStripes == nil { if opts.ShowRowStripes == nil {
opts.ShowRowStripes = boolPtr(true) opts.ShowRowStripes = boolPtr(true)
} }
if err = checkTableName(opts.Name); err != nil { if err = checkDefinedName(opts.Name); err != nil {
return opts, err return opts, err
} }
return opts, err return opts, err
@ -75,6 +83,23 @@ func (f *File) AddTable(sheet string, table *Table) error {
if err != nil { if err != nil {
return err return err
} }
var exist bool
f.Pkg.Range(func(k, v interface{}) bool {
if strings.Contains(k.(string), "xl/tables/table") {
var t xlsxTable
if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(v.([]byte)))).
Decode(&t); err != nil && err != io.EOF {
return true
}
if exist = t.Name == options.Name; exist {
return false
}
}
return true
})
if exist {
return ErrExistsTableName
}
// Coordinate conversion, convert C1:B3 to 2,0,1,2. // Coordinate conversion, convert C1:B3 to 2,0,1,2.
coordinates, err := rangeRefToCoordinates(options.Range) coordinates, err := rangeRefToCoordinates(options.Range)
if err != nil { if err != nil {
@ -99,13 +124,125 @@ func (f *File) AddTable(sheet string, table *Table) error {
return f.addContentTypePart(tableID, "table") return f.addContentTypePart(tableID, "table")
} }
// GetTables provides the method to get all tables in a worksheet by given
// worksheet name.
func (f *File) GetTables(sheet string) ([]Table, error) {
var tables []Table
ws, err := f.workSheetReader(sheet)
if err != nil {
return tables, err
}
if ws.TableParts == nil {
return tables, err
}
for _, tbl := range ws.TableParts.TableParts {
if tbl != nil {
target := f.getSheetRelationshipsTargetByID(sheet, tbl.RID)
tableXML := strings.ReplaceAll(target, "..", "xl")
content, ok := f.Pkg.Load(tableXML)
if !ok {
continue
}
var t xlsxTable
if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(content.([]byte)))).
Decode(&t); err != nil && err != io.EOF {
return tables, err
}
table := Table{
rID: tbl.RID,
tID: t.ID,
tableXML: tableXML,
Range: t.Ref,
Name: t.Name,
}
if t.TableStyleInfo != nil {
table.StyleName = t.TableStyleInfo.Name
table.ShowColumnStripes = t.TableStyleInfo.ShowColumnStripes
table.ShowFirstColumn = t.TableStyleInfo.ShowFirstColumn
table.ShowLastColumn = t.TableStyleInfo.ShowLastColumn
table.ShowRowStripes = &t.TableStyleInfo.ShowRowStripes
}
tables = append(tables, table)
}
}
return tables, err
}
// DeleteTable provides the method to delete table by given table name.
func (f *File) DeleteTable(name string) error {
if err := checkDefinedName(name); err != nil {
return err
}
tbls, err := f.getTables()
if err != nil {
return err
}
for sheet, tables := range tbls {
for _, table := range tables {
if table.Name != name {
continue
}
ws, _ := f.workSheetReader(sheet)
for i, tbl := range ws.TableParts.TableParts {
if tbl.RID == table.rID {
ws.TableParts.TableParts = append(ws.TableParts.TableParts[:i], ws.TableParts.TableParts[i+1:]...)
f.Pkg.Delete(table.tableXML)
_ = f.removeContentTypesPart(ContentTypeSpreadSheetMLTable, "/"+table.tableXML)
f.deleteSheetRelationships(sheet, tbl.RID)
break
}
}
if ws.TableParts.Count = len(ws.TableParts.TableParts); ws.TableParts.Count == 0 {
ws.TableParts = nil
}
return err
}
}
return newNoExistTableError(name)
}
// getTables provides a function to get all tables in a workbook.
func (f *File) getTables() (map[string][]Table, error) {
tables := map[string][]Table{}
for _, sheetName := range f.GetSheetList() {
tbls, err := f.GetTables(sheetName)
e := ErrSheetNotExist{sheetName}
if err != nil && err.Error() != newNotWorksheetError(sheetName).Error() && err.Error() != e.Error() {
return tables, err
}
tables[sheetName] = append(tables[sheetName], tbls...)
}
return tables, nil
}
// countTables provides a function to get table files count storage in the // countTables provides a function to get table files count storage in the
// folder xl/tables. // folder xl/tables.
func (f *File) countTables() int { func (f *File) countTables() int {
count := 0 count := 0
f.Pkg.Range(func(k, v interface{}) bool { f.Pkg.Range(func(k, v interface{}) bool {
if strings.Contains(k.(string), "xl/tables/tableSingleCells") {
var cells xlsxSingleXMLCells
if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(v.([]byte)))).
Decode(&cells); err != nil && err != io.EOF {
count++
return true
}
for _, cell := range cells.SingleXmlCell {
if count < cell.ID {
count = cell.ID
}
}
}
if strings.Contains(k.(string), "xl/tables/table") { if strings.Contains(k.(string), "xl/tables/table") {
count++ var t xlsxTable
if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(v.([]byte)))).
Decode(&t); err != nil && err != io.EOF {
count++
return true
}
if count < t.ID {
count = t.ID
}
} }
return true return true
}) })
@ -130,58 +267,87 @@ func (f *File) addSheetTable(sheet string, rID int) error {
return err return err
} }
// setTableHeader provides a function to set cells value in header row for the // setTableColumns provides a function to set cells value in header row for the
// table. // table.
func (f *File) setTableHeader(sheet string, showHeaderRow bool, x1, y1, x2 int) ([]*xlsxTableColumn, error) { func (f *File) setTableColumns(sheet string, showHeaderRow bool, x1, y1, x2 int, tbl *xlsxTable) error {
var ( var (
tableColumns []*xlsxTableColumn idx int
idx int header []string
tableColumns []*xlsxTableColumn
getTableColumn = func(name string) *xlsxTableColumn {
if tbl != nil && tbl.TableColumns != nil {
for _, column := range tbl.TableColumns.TableColumn {
if column.Name == name {
return column
}
}
}
return nil
}
) )
for i := x1; i <= x2; i++ { for i := x1; i <= x2; i++ {
idx++ idx++
cell, err := CoordinatesToCellName(i, y1) cell, err := CoordinatesToCellName(i, y1)
if err != nil { if err != nil {
return tableColumns, err return err
} }
name, _ := f.GetCellValue(sheet, cell) name, _ := f.GetCellValue(sheet, cell, Options{RawCellValue: true})
if _, err := strconv.Atoi(name); err == nil { if _, err := strconv.Atoi(name); err == nil {
if showHeaderRow { if showHeaderRow {
_ = f.SetCellStr(sheet, cell, name) _ = f.SetCellStr(sheet, cell, name)
} }
} }
if name == "" { if name == "" || inStrSlice(header, name, true) != -1 {
name = "Column" + strconv.Itoa(idx) name = "Column" + strconv.Itoa(idx)
if showHeaderRow { if showHeaderRow {
_ = f.SetCellStr(sheet, cell, name) _ = f.SetCellStr(sheet, cell, name)
} }
} }
header = append(header, name)
if column := getTableColumn(name); column != nil {
column.ID, column.DataDxfID, column.QueryTableFieldID = idx, 0, 0
tableColumns = append(tableColumns, column)
continue
}
tableColumns = append(tableColumns, &xlsxTableColumn{ tableColumns = append(tableColumns, &xlsxTableColumn{
ID: idx, ID: idx,
Name: name, Name: name,
}) })
} }
return tableColumns, nil tbl.TableColumns = &xlsxTableColumns{
Count: len(tableColumns),
TableColumn: tableColumns,
}
return nil
} }
// checkSheetName check whether there are illegal characters in the table name. // checkDefinedName check whether there are illegal characters in the defined
// Verify that the name: // name or table name. Verify that the name:
// 1. Starts with a letter or underscore (_) // 1. Starts with a letter or underscore (_)
// 2. Doesn't include a space or character that isn't allowed // 2. Doesn't include a space or character that isn't allowed
func checkTableName(name string) error { func checkDefinedName(name string) error {
if utf8.RuneCountInString(name) > MaxFieldLength { if utf8.RuneCountInString(name) > MaxFieldLength {
return ErrTableNameLength return ErrNameLength
}
inCodeRange := func(code int, tbl []int) bool {
for i := 0; i < len(tbl); i += 2 {
if tbl[i] <= code && code <= tbl[i+1] {
return true
}
}
return false
} }
for i, c := range name { for i, c := range name {
if string(c) == "_" { if i == 0 {
if inCodeRange(int(c), supportedDefinedNameAtStartCharCodeRange) {
continue
}
return newInvalidNameError(name)
}
if inCodeRange(int(c), supportedDefinedNameAfterStartCharCodeRange) {
continue continue
} }
if unicode.IsLetter(c) { return newInvalidNameError(name)
continue
}
if i > 0 && unicode.IsDigit(c) {
continue
}
return newInvalidTableNameError(name)
} }
return nil return nil
} }
@ -198,11 +364,10 @@ func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, opts *Tab
y1++ y1++
} }
// Correct table range reference, such correct C1:B3 to B1:C3. // Correct table range reference, such correct C1:B3 to B1:C3.
ref, err := f.coordinatesToRangeRef([]int{x1, y1, x2, y2}) ref, err := coordinatesToRangeRef([]int{x1, y1, x2, y2})
if err != nil { if err != nil {
return err return err
} }
tableColumns, _ := f.setTableHeader(sheet, !hideHeaderRow, x1, y1, x2)
name := opts.Name name := opts.Name
if name == "" { if name == "" {
name = "Table" + strconv.Itoa(i) name = "Table" + strconv.Itoa(i)
@ -216,10 +381,6 @@ func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, opts *Tab
AutoFilter: &xlsxAutoFilter{ AutoFilter: &xlsxAutoFilter{
Ref: ref, Ref: ref,
}, },
TableColumns: &xlsxTableColumns{
Count: len(tableColumns),
TableColumn: tableColumns,
},
TableStyleInfo: &xlsxTableStyleInfo{ TableStyleInfo: &xlsxTableStyleInfo{
Name: opts.StyleName, Name: opts.StyleName,
ShowFirstColumn: opts.ShowFirstColumn, ShowFirstColumn: opts.ShowFirstColumn,
@ -228,13 +389,14 @@ func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, opts *Tab
ShowColumnStripes: opts.ShowColumnStripes, ShowColumnStripes: opts.ShowColumnStripes,
}, },
} }
_ = f.setTableColumns(sheet, !hideHeaderRow, x1, y1, x2, &t)
if hideHeaderRow { if hideHeaderRow {
t.AutoFilter = nil t.AutoFilter = nil
t.HeaderRowCount = intPtr(0) t.HeaderRowCount = intPtr(0)
} }
table, _ := xml.Marshal(t) table, err := xml.Marshal(t)
f.saveFileList(tableXML, table) f.saveFileList(tableXML, table)
return nil return err
} }
// AutoFilter provides the method to add auto filter in a worksheet by given // AutoFilter provides the method to add auto filter in a worksheet by given
@ -294,7 +456,7 @@ func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, opts *Tab
// x == *b // ends with b // x == *b // ends with b
// x != *b // doesn't end with b // x != *b // doesn't end with b
// x == *b* // contains b // x == *b* // contains b
// x != *b* // doesn't contains b // x != *b* // doesn't contain b
// //
// You can also use '*' to match any character or number and '?' to match any // You can also use '*' to match any character or number and '?' to match any
// single character or number. No other regular expression quantifier is // single character or number. No other regular expression quantifier is
@ -315,8 +477,7 @@ func (f *File) AutoFilter(sheet, rangeRef string, opts []AutoFilterOptions) erro
} }
_ = sortCoordinates(coordinates) _ = sortCoordinates(coordinates)
// Correct reference range, such correct C1:B3 to B1:C3. // Correct reference range, such correct C1:B3 to B1:C3.
ref, _ := f.coordinatesToRangeRef(coordinates, true) ref, _ := coordinatesToRangeRef(coordinates, true)
filterDB := "_xlnm._FilterDatabase"
wb, err := f.workbookReader() wb, err := f.workbookReader()
if err != nil { if err != nil {
return err return err
@ -327,7 +488,7 @@ func (f *File) AutoFilter(sheet, rangeRef string, opts []AutoFilterOptions) erro
} }
filterRange := fmt.Sprintf("'%s'!%s", sheet, ref) filterRange := fmt.Sprintf("'%s'!%s", sheet, ref)
d := xlsxDefinedName{ d := xlsxDefinedName{
Name: filterDB, Name: builtInDefinedNames[3],
Hidden: true, Hidden: true,
LocalSheetID: intPtr(sheetID), LocalSheetID: intPtr(sheetID),
Data: filterRange, Data: filterRange,
@ -339,8 +500,11 @@ func (f *File) AutoFilter(sheet, rangeRef string, opts []AutoFilterOptions) erro
} else { } else {
var definedNameExists bool var definedNameExists bool
for idx := range wb.DefinedNames.DefinedName { for idx := range wb.DefinedNames.DefinedName {
definedName := wb.DefinedNames.DefinedName[idx] definedName, localSheetID := wb.DefinedNames.DefinedName[idx], 0
if definedName.Name == filterDB && *definedName.LocalSheetID == sheetID && definedName.Hidden { if definedName.LocalSheetID != nil {
localSheetID = *definedName.LocalSheetID
}
if definedName.Name == builtInDefinedNames[3] && localSheetID == sheetID && definedName.Hidden {
wb.DefinedNames.DefinedName[idx].Data = filterRange wb.DefinedNames.DefinedName[idx].Data = filterRange
definedNameExists = true definedNameExists = true
} }
@ -378,13 +542,12 @@ func (f *File) autoFilter(sheet, ref string, columns, col int, opts []AutoFilter
} }
offset := fsCol - col offset := fsCol - col
if offset < 0 || offset > columns { if offset < 0 || offset > columns {
return fmt.Errorf("incorrect index of column '%s'", opt.Column) return newInvalidAutoFilterColumnError(opt.Column)
} }
fc := &xlsxFilterColumn{ColID: offset} fc := &xlsxFilterColumn{ColID: offset}
re := regexp.MustCompile(`"(?:[^"]|"")*"|\S+`) token := expressionFormat.FindAllString(opt.Expression, -1)
token := re.FindAllString(opt.Expression, -1)
if len(token) != 3 && len(token) != 7 { if len(token) != 3 && len(token) != 7 {
return fmt.Errorf("incorrect number of tokens in criteria '%s'", opt.Expression) return newInvalidAutoFilterExpError(opt.Expression)
} }
expressions, tokens, err := f.parseFilterExpression(opt.Expression, token) expressions, tokens, err := f.parseFilterExpression(opt.Expression, token)
if err != nil { if err != nil {
@ -405,22 +568,23 @@ func (f *File) writeAutoFilter(fc *xlsxFilterColumn, exp []int, tokens []string)
var filters []*xlsxFilter var filters []*xlsxFilter
filters = append(filters, &xlsxFilter{Val: tokens[0]}) filters = append(filters, &xlsxFilter{Val: tokens[0]})
fc.Filters = &xlsxFilters{Filter: filters} fc.Filters = &xlsxFilters{Filter: filters}
} else if len(exp) == 3 && exp[0] == 2 && exp[1] == 1 && exp[2] == 2 { return
}
if len(exp) == 3 && exp[0] == 2 && exp[1] == 1 && exp[2] == 2 {
// Double equality with "or" operator. // Double equality with "or" operator.
var filters []*xlsxFilter var filters []*xlsxFilter
for _, v := range tokens { for _, v := range tokens {
filters = append(filters, &xlsxFilter{Val: v}) filters = append(filters, &xlsxFilter{Val: v})
} }
fc.Filters = &xlsxFilters{Filter: filters} fc.Filters = &xlsxFilters{Filter: filters}
} else { return
// Non default custom filter. }
expRel := map[int]int{0: 0, 1: 2} // Non default custom filter.
andRel := map[int]bool{0: true, 1: false} expRel, andRel := map[int]int{0: 0, 1: 2}, map[int]bool{0: true, 1: false}
for k, v := range tokens { for k, v := range tokens {
f.writeCustomFilter(fc, exp[expRel[k]], v) f.writeCustomFilter(fc, exp[expRel[k]], v)
if k == 1 { if k == 1 {
fc.CustomFilters.And = andRel[exp[k]] fc.CustomFilters.And = andRel[exp[k]]
}
} }
} }
} }
@ -442,11 +606,11 @@ func (f *File) writeCustomFilter(fc *xlsxFilterColumn, operator int, val string)
} }
if fc.CustomFilters != nil { if fc.CustomFilters != nil {
fc.CustomFilters.CustomFilter = append(fc.CustomFilters.CustomFilter, &customFilter) fc.CustomFilters.CustomFilter = append(fc.CustomFilters.CustomFilter, &customFilter)
} else { return
var customFilters []*xlsxCustomFilter
customFilters = append(customFilters, &customFilter)
fc.CustomFilters = &xlsxCustomFilters{CustomFilter: customFilters}
} }
var customFilters []*xlsxCustomFilter
customFilters = append(customFilters, &customFilter)
fc.CustomFilters = &xlsxCustomFilters{CustomFilter: customFilters}
} }
// parseFilterExpression provides a function to converts the tokens of a // parseFilterExpression provides a function to converts the tokens of a
@ -463,10 +627,8 @@ func (f *File) parseFilterExpression(expression string, tokens []string) ([]int,
if len(tokens) == 7 { if len(tokens) == 7 {
// The number of tokens will be either 3 (for 1 expression) or 7 (for 2 // The number of tokens will be either 3 (for 1 expression) or 7 (for 2
// expressions). // expressions).
conditional := 0 conditional, c := 0, tokens[3]
c := tokens[3] if conditionFormat.MatchString(c) {
re, _ := regexp.Match(`(or|\|\|)`, []byte(c))
if re {
conditional = 1 conditional = 1
} }
expression1, token1, err := f.parseFilterTokens(expression, tokens[:3]) expression1, token1, err := f.parseFilterTokens(expression, tokens[:3])
@ -477,17 +639,13 @@ func (f *File) parseFilterExpression(expression string, tokens []string) ([]int,
if err != nil { if err != nil {
return expressions, t, err return expressions, t, err
} }
expressions = []int{expression1[0], conditional, expression2[0]} return []int{expression1[0], conditional, expression2[0]}, []string{token1, token2}, nil
t = []string{token1, token2}
} else {
exp, token, err := f.parseFilterTokens(expression, tokens)
if err != nil {
return expressions, t, err
}
expressions = exp
t = []string{token}
} }
return expressions, t, nil exp, token, err := f.parseFilterTokens(expression, tokens)
if err != nil {
return expressions, t, err
}
return exp, []string{token}, nil
} }
// parseFilterTokens provides a function to parse the 3 tokens of a filter // parseFilterTokens provides a function to parse the 3 tokens of a filter
@ -510,15 +668,15 @@ func (f *File) parseFilterTokens(expression string, tokens []string) ([]int, str
operator, ok := operators[strings.ToLower(tokens[1])] operator, ok := operators[strings.ToLower(tokens[1])]
if !ok { if !ok {
// Convert the operator from a number to a descriptive string. // Convert the operator from a number to a descriptive string.
return []int{}, "", fmt.Errorf("unknown operator: %s", tokens[1]) return []int{}, "", newUnknownFilterTokenError(tokens[1])
} }
token := tokens[2] token := tokens[2]
// Special handling for Blanks/NonBlanks. // Special handling for Blanks/NonBlanks.
re, _ := regexp.Match("blanks|nonblanks", []byte(strings.ToLower(token))) re := blankFormat.MatchString(strings.ToLower(token))
if re { if re {
// Only allow Equals or NotEqual in this context. // Only allow Equals or NotEqual in this context.
if operator != 2 && operator != 5 { if operator != 2 && operator != 5 {
return []int{operator}, token, fmt.Errorf("the operator '%s' in expression '%s' is not valid in relation to Blanks/NonBlanks'", tokens[1], expression) return []int{operator}, token, newInvalidAutoFilterOperatorError(tokens[1], expression)
} }
token = strings.ToLower(token) token = strings.ToLower(token)
// The operator should always be 2 (=) to flag a "simple" equality in // The operator should always be 2 (=) to flag a "simple" equality in
@ -539,8 +697,7 @@ func (f *File) parseFilterTokens(expression string, tokens []string) ([]int, str
} }
// If the string token contains an Excel match character then change the // If the string token contains an Excel match character then change the
// operator type to indicate a non "simple" equality. // operator type to indicate a non "simple" equality.
re, _ = regexp.Match("[*?]", []byte(token)) if re = matchFormat.MatchString(token); operator == 2 && re {
if operator == 2 && re {
operator = 22 operator = 22
} }
return []int{operator}, token, nil return []int{operator}, token, nil

View File

@ -27,7 +27,13 @@ func TestAddTable(t *testing.T) {
ShowHeaderRow: boolPtr(false), ShowHeaderRow: boolPtr(false),
})) }))
assert.NoError(t, f.AddTable("Sheet2", &Table{Range: "F1:F1", StyleName: "TableStyleMedium8"})) assert.NoError(t, f.AddTable("Sheet2", &Table{Range: "F1:F1", StyleName: "TableStyleMedium8"}))
// Test get tables in worksheet
tables, err := f.GetTables("Sheet2")
assert.Len(t, tables, 3)
assert.NoError(t, err)
// Test add table with already exist table name
assert.Equal(t, f.AddTable("Sheet2", &Table{Name: "Table1"}), ErrExistsTableName)
// Test add table with invalid table options // Test add table with invalid table options
assert.Equal(t, f.AddTable("Sheet1", nil), ErrParameterInvalid) assert.Equal(t, f.AddTable("Sheet1", nil), ErrParameterInvalid)
// Test add table in not exist worksheet // Test add table in not exist worksheet
@ -39,36 +45,101 @@ func TestAddTable(t *testing.T) {
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddTable.xlsx"))) assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddTable.xlsx")))
// Test add table with invalid sheet name // Test add table with invalid sheet name
assert.EqualError(t, f.AddTable("Sheet:1", &Table{Range: "B26:A21"}), ErrSheetNameInvalid.Error()) assert.Equal(t, ErrSheetNameInvalid, f.AddTable("Sheet:1", &Table{Range: "B26:A21"}))
// Test addTable with illegal cell reference // Test addTable with illegal cell reference
f = NewFile() f = NewFile()
assert.EqualError(t, f.addTable("sheet1", "", 0, 0, 0, 0, 0, nil), "invalid cell reference [0, 0]") assert.Equal(t, newCoordinatesToCellNameError(0, 0), f.addTable("sheet1", "", 0, 0, 0, 0, 0, nil))
assert.EqualError(t, f.addTable("sheet1", "", 1, 1, 0, 0, 0, nil), "invalid cell reference [0, 0]") assert.Equal(t, newCoordinatesToCellNameError(0, 0), f.addTable("sheet1", "", 1, 1, 0, 0, 0, nil))
// Test add table with invalid table name // Test set defined name and add table with invalid name
for _, cases := range []struct { for _, cases := range []struct {
name string name string
err error err error
}{ }{
{name: "1Table", err: newInvalidTableNameError("1Table")}, {name: "1Table", err: newInvalidNameError("1Table")},
{name: "-Table", err: newInvalidTableNameError("-Table")}, {name: "-Table", err: newInvalidNameError("-Table")},
{name: "'Table", err: newInvalidTableNameError("'Table")}, {name: "'Table", err: newInvalidNameError("'Table")},
{name: "Table 1", err: newInvalidTableNameError("Table 1")}, {name: "Table 1", err: newInvalidNameError("Table 1")},
{name: "A&B", err: newInvalidTableNameError("A&B")}, {name: "A&B", err: newInvalidNameError("A&B")},
{name: "_1Table'", err: newInvalidTableNameError("_1Table'")}, {name: "_1Table'", err: newInvalidNameError("_1Table'")},
{name: "\u0f5f\u0fb3\u0f0b\u0f21", err: newInvalidTableNameError("\u0f5f\u0fb3\u0f0b\u0f21")}, {name: "\u0f5f\u0fb3\u0f0b\u0f21", err: newInvalidNameError("\u0f5f\u0fb3\u0f0b\u0f21")},
{name: strings.Repeat("c", MaxFieldLength+1), err: ErrTableNameLength}, {name: strings.Repeat("c", MaxFieldLength+1), err: ErrNameLength},
} { } {
assert.EqualError(t, f.AddTable("Sheet1", &Table{ assert.Equal(t, cases.err, f.AddTable("Sheet1", &Table{
Range: "A1:B2", Range: "A1:B2",
Name: cases.name, Name: cases.name,
}), cases.err.Error()) }))
assert.Equal(t, cases.err, f.SetDefinedName(&DefinedName{
Name: cases.name, RefersTo: "Sheet1!$A$2:$D$5",
}))
} }
// Test check duplicate table name with unsupported charset table parts
f = NewFile()
f.Pkg.Store("xl/tables/table1.xml", MacintoshCyrillicCharset)
assert.NoError(t, f.AddTable("Sheet1", &Table{Range: "A1:B2"}))
assert.NoError(t, f.Close())
f = NewFile()
// Test add table with workbook with single cells parts
f.Pkg.Store("xl/tables/tableSingleCells1.xml", []byte("<singleXmlCells><singleXmlCell id=\"2\" r=\"A1\" connectionId=\"2\" /></singleXmlCells>"))
assert.NoError(t, f.AddTable("Sheet1", &Table{Range: "A1:B2"}))
// Test add table with workbook with unsupported charset single cells parts
f.Pkg.Store("xl/tables/tableSingleCells1.xml", MacintoshCyrillicCharset)
assert.NoError(t, f.AddTable("Sheet1", &Table{Range: "A1:B2"}))
assert.NoError(t, f.Close())
} }
func TestSetTableHeader(t *testing.T) { func TestGetTables(t *testing.T) {
f := NewFile() f := NewFile()
_, err := f.setTableHeader("Sheet1", true, 1, 0, 1) // Test get tables in none table worksheet
assert.EqualError(t, err, "invalid cell reference [1, 0]") tables, err := f.GetTables("Sheet1")
assert.Len(t, tables, 0)
assert.NoError(t, err)
// Test get tables in not exist worksheet
_, err = f.GetTables("SheetN")
assert.EqualError(t, err, "sheet SheetN does not exist")
// Test adjust table with unsupported charset
assert.NoError(t, f.AddTable("Sheet1", &Table{Range: "B26:A21"}))
f.Pkg.Store("xl/tables/table1.xml", MacintoshCyrillicCharset)
_, err = f.GetTables("Sheet1")
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
// Test adjust table with no exist table parts
f.Pkg.Delete("xl/tables/table1.xml")
tables, err = f.GetTables("Sheet1")
assert.Len(t, tables, 0)
assert.NoError(t, err)
}
func TestDeleteTable(t *testing.T) {
f := NewFile()
assert.NoError(t, f.AddTable("Sheet1", &Table{Range: "A1:B4", Name: "Table1"}))
assert.NoError(t, f.AddTable("Sheet1", &Table{Range: "B26:A21", Name: "Table2"}))
assert.NoError(t, f.DeleteTable("Table2"))
assert.NoError(t, f.DeleteTable("Table1"))
// Test delete table with invalid table name
assert.Equal(t, newInvalidNameError("Table 1"), f.DeleteTable("Table 1"))
// Test delete table with no exist table name
assert.Equal(t, newNoExistTableError("Table"), f.DeleteTable("Table"))
// Test delete table with unsupported charset
f.Sheet.Delete("xl/worksheets/sheet1.xml")
f.Pkg.Store("xl/worksheets/sheet1.xml", MacintoshCyrillicCharset)
assert.EqualError(t, f.DeleteTable("Table1"), "XML syntax error on line 1: invalid UTF-8")
// Test delete table without deleting table header
f = NewFile()
assert.NoError(t, f.SetCellValue("Sheet1", "A1", "Date"))
assert.NoError(t, f.SetCellValue("Sheet1", "B1", "Values"))
assert.NoError(t, f.UpdateLinkedValue())
assert.NoError(t, f.AddTable("Sheet1", &Table{Range: "A1:B2", Name: "Table1"}))
assert.NoError(t, f.DeleteTable("Table1"))
val, err := f.GetCellValue("Sheet1", "A1")
assert.NoError(t, err)
assert.Equal(t, "Date", val)
val, err = f.GetCellValue("Sheet1", "B1")
assert.NoError(t, err)
assert.Equal(t, "Values", val)
}
func TestSetTableColumns(t *testing.T) {
f := NewFile()
assert.Equal(t, newCoordinatesToCellNameError(1, 0), f.setTableColumns("Sheet1", true, 1, 0, 1, nil))
} }
func TestAutoFilter(t *testing.T) { func TestAutoFilter(t *testing.T) {
@ -93,14 +164,18 @@ func TestAutoFilter(t *testing.T) {
} }
// Test add auto filter with invalid sheet name // Test add auto filter with invalid sheet name
assert.EqualError(t, f.AutoFilter("Sheet:1", "A1:B1", nil), ErrSheetNameInvalid.Error()) assert.Equal(t, ErrSheetNameInvalid, f.AutoFilter("Sheet:1", "A1:B1", nil))
// Test add auto filter with illegal cell reference // Test add auto filter with illegal cell reference
assert.EqualError(t, f.AutoFilter("Sheet1", "A:B1", nil), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error()) assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.AutoFilter("Sheet1", "A:B1", nil))
assert.EqualError(t, f.AutoFilter("Sheet1", "A1:B", nil), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")).Error()) assert.Equal(t, newCellNameToCoordinatesError("B", newInvalidCellNameError("B")), f.AutoFilter("Sheet1", "A1:B", nil))
// Test add auto filter with unsupported charset workbook // Test add auto filter with unsupported charset workbook
f.WorkBook = nil f.WorkBook = nil
f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset) f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset)
assert.EqualError(t, f.AutoFilter("Sheet1", "D4:B1", nil), "XML syntax error on line 1: invalid UTF-8") assert.EqualError(t, f.AutoFilter("Sheet1", "D4:B1", nil), "XML syntax error on line 1: invalid UTF-8")
// Test add auto filter with empty local sheet ID
f = NewFile()
f.WorkBook = &xlsxWorkbook{DefinedNames: &xlsxDefinedNames{DefinedName: []xlsxDefinedName{{Name: builtInDefinedNames[3], Hidden: true}}}}
assert.NoError(t, f.AutoFilter("Sheet1", "A1:B1", nil))
} }
func TestAutoFilterError(t *testing.T) { func TestAutoFilterError(t *testing.T) {
@ -122,22 +197,22 @@ func TestAutoFilterError(t *testing.T) {
}) })
} }
assert.EqualError(t, f.autoFilter("SheetN", "A1", 1, 1, []AutoFilterOptions{{ assert.Equal(t, ErrSheetNotExist{"SheetN"}, f.autoFilter("SheetN", "A1", 1, 1, []AutoFilterOptions{{
Column: "A", Column: "A",
Expression: "", Expression: "",
}}), "sheet SheetN does not exist") }}))
assert.EqualError(t, f.autoFilter("Sheet1", "A1", 1, 1, []AutoFilterOptions{{ assert.Equal(t, newInvalidColumnNameError("-"), f.autoFilter("Sheet1", "A1", 1, 1, []AutoFilterOptions{{
Column: "-", Column: "-",
Expression: "-", Expression: "-",
}}), newInvalidColumnNameError("-").Error()) }}))
assert.EqualError(t, f.autoFilter("Sheet1", "A1", 1, 100, []AutoFilterOptions{{ assert.Equal(t, newInvalidAutoFilterColumnError("A"), f.autoFilter("Sheet1", "A1", 1, 100, []AutoFilterOptions{{
Column: "A", Column: "A",
Expression: "-", Expression: "-",
}}), `incorrect index of column 'A'`) }}))
assert.EqualError(t, f.autoFilter("Sheet1", "A1", 1, 1, []AutoFilterOptions{{ assert.Equal(t, newInvalidAutoFilterExpError("-"), f.autoFilter("Sheet1", "A1", 1, 1, []AutoFilterOptions{{
Column: "A", Column: "A",
Expression: "-", Expression: "-",
}}), `incorrect number of tokens in criteria '-'`) }}))
} }
func TestParseFilterTokens(t *testing.T) { func TestParseFilterTokens(t *testing.T) {
@ -147,5 +222,5 @@ func TestParseFilterTokens(t *testing.T) {
assert.EqualError(t, err, "unknown operator: !") assert.EqualError(t, err, "unknown operator: !")
// Test invalid operator in context // Test invalid operator in context
_, _, err = f.parseFilterTokens("", []string{"", "<", "x != blanks"}) _, _, err = f.parseFilterTokens("", []string{"", "<", "x != blanks"})
assert.EqualError(t, err, "the operator '<' in expression '' is not valid in relation to Blanks/NonBlanks'") assert.Equal(t, newInvalidAutoFilterOperatorError("<", ""), err)
} }

View File

@ -1,4 +1,4 @@
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in // this source code is governed by a BSD-style license that can be found in
// the LICENSE file. // the LICENSE file.
// //
@ -7,26 +7,510 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming // Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of // API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.16 or later. // data. This library needs Go version 1.18 or later.
// //
// This file contains default templates for XML files we don't yet populated // This file contains default templates for XML files we don't yet populated
// based on content. // based on content.
package excelize package excelize
const ( import "encoding/xml"
defaultXMLPathContentTypes = "[Content_Types].xml"
defaultXMLPathDocPropsApp = "docProps/app.xml" // Source relationship and namespace list, associated prefixes and schema in which it was
defaultXMLPathDocPropsCore = "docProps/core.xml" // introduced.
defaultXMLPathCalcChain = "xl/calcChain.xml" var (
defaultXMLPathSharedStrings = "xl/sharedStrings.xml" NameSpaceDocumentPropertiesVariantTypes = xml.Attr{Name: xml.Name{Local: "vt", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"}
defaultXMLPathStyles = "xl/styles.xml" NameSpaceDrawing2016SVG = xml.Attr{Name: xml.Name{Local: "asvg", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/drawing/2016/SVG/main"}
defaultXMLPathTheme = "xl/theme/theme1.xml" NameSpaceDrawingML = xml.Attr{Name: xml.Name{Local: "a", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/drawingml/2006/main"}
defaultXMLPathWorkbook = "xl/workbook.xml" NameSpaceDrawingMLA14 = xml.Attr{Name: xml.Name{Local: "a14", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/drawing/2010/main"}
defaultXMLPathWorkbookRels = "xl/_rels/workbook.xml.rels" NameSpaceDrawingMLChart = xml.Attr{Name: xml.Name{Local: "c", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/drawingml/2006/chart"}
defaultTempFileSST = "sharedStrings" NameSpaceDrawingMLSlicer = xml.Attr{Name: xml.Name{Local: "sle", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/drawing/2010/slicer"}
NameSpaceDrawingMLSlicerX15 = xml.Attr{Name: xml.Name{Local: "sle15", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/drawing/2012/slicer"}
NameSpaceDrawingMLSpreadSheet = xml.Attr{Name: xml.Name{Local: "xdr", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing"}
NameSpaceMacExcel2008Main = xml.Attr{Name: xml.Name{Local: "mx", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/mac/excel/2008/main"}
NameSpaceSpreadSheet = xml.Attr{Name: xml.Name{Local: "xmlns"}, Value: "http://schemas.openxmlformats.org/spreadsheetml/2006/main"}
NameSpaceSpreadSheetExcel2006Main = xml.Attr{Name: xml.Name{Local: "xne", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/excel/2006/main"}
NameSpaceSpreadSheetX14 = xml.Attr{Name: xml.Name{Local: "x14", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main"}
NameSpaceSpreadSheetX15 = xml.Attr{Name: xml.Name{Local: "x15", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/spreadsheetml/2010/11/main"}
NameSpaceSpreadSheetXR10 = xml.Attr{Name: xml.Name{Local: "xr10", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/spreadsheetml/2016/revision10"}
SourceRelationship = xml.Attr{Name: xml.Name{Local: "r", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/officeDocument/2006/relationships"}
SourceRelationshipChart20070802 = xml.Attr{Name: xml.Name{Local: "c14", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/drawing/2007/8/2/chart"}
SourceRelationshipChart2014 = xml.Attr{Name: xml.Name{Local: "c16", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/drawing/2014/chart"}
SourceRelationshipChart201506 = xml.Attr{Name: xml.Name{Local: "c16r2", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/drawing/2015/06/chart"}
SourceRelationshipCompatibility = xml.Attr{Name: xml.Name{Local: "mc", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/markup-compatibility/2006"}
) )
// Source relationship and namespace.
const (
ContentTypeAddinMacro = "application/vnd.ms-excel.addin.macroEnabled.main+xml"
ContentTypeDrawing = "application/vnd.openxmlformats-officedocument.drawing+xml"
ContentTypeDrawingML = "application/vnd.openxmlformats-officedocument.drawingml.chart+xml"
ContentTypeMacro = "application/vnd.ms-excel.sheet.macroEnabled.main+xml"
ContentTypeRelationships = "application/vnd.openxmlformats-package.relationships+xml"
ContentTypeSheetML = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"
ContentTypeSlicer = "application/vnd.ms-excel.slicer+xml"
ContentTypeSlicerCache = "application/vnd.ms-excel.slicerCache+xml"
ContentTypeSpreadSheetMLChartsheet = "application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml"
ContentTypeSpreadSheetMLComments = "application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml"
ContentTypeSpreadSheetMLPivotCacheDefinition = "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml"
ContentTypeSpreadSheetMLPivotTable = "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml"
ContentTypeSpreadSheetMLSharedStrings = "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"
ContentTypeSpreadSheetMLTable = "application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml"
ContentTypeSpreadSheetMLWorksheet = "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"
ContentTypeTemplate = "application/vnd.openxmlformats-officedocument.spreadsheetml.template.main+xml"
ContentTypeTemplateMacro = "application/vnd.ms-excel.template.macroEnabled.main+xml"
ContentTypeVBA = "application/vnd.ms-office.vbaProject"
ContentTypeVML = "application/vnd.openxmlformats-officedocument.vmlDrawing"
NameSpaceDrawingMLMain = "http://schemas.openxmlformats.org/drawingml/2006/main"
NameSpaceDublinCore = "http://purl.org/dc/elements/1.1/"
NameSpaceDublinCoreMetadataInitiative = "http://purl.org/dc/dcmitype/"
NameSpaceDublinCoreTerms = "http://purl.org/dc/terms/"
NameSpaceExtendedProperties = "http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"
NameSpaceXML = "http://www.w3.org/XML/1998/namespace"
NameSpaceXMLSchemaInstance = "http://www.w3.org/2001/XMLSchema-instance"
SourceRelationshipChart = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart"
SourceRelationshipChartsheet = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chartsheet"
SourceRelationshipComments = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments"
SourceRelationshipDialogsheet = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/dialogsheet"
SourceRelationshipDrawingML = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing"
SourceRelationshipDrawingVML = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing"
SourceRelationshipExtendProperties = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties"
SourceRelationshipHyperLink = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink"
SourceRelationshipImage = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image"
SourceRelationshipOfficeDocument = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument"
SourceRelationshipPivotCache = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition"
SourceRelationshipPivotTable = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable"
SourceRelationshipSharedStrings = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings"
SourceRelationshipSlicer = "http://schemas.microsoft.com/office/2007/relationships/slicer"
SourceRelationshipSlicerCache = "http://schemas.microsoft.com/office/2007/relationships/slicerCache"
SourceRelationshipTable = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/table"
SourceRelationshipVBAProject = "http://schemas.microsoft.com/office/2006/relationships/vbaProject"
SourceRelationshipWorkSheet = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"
StrictNameSpaceDocumentPropertiesVariantTypes = "http://purl.oclc.org/ooxml/officeDocument/docPropsVTypes"
StrictNameSpaceDrawingMLMain = "http://purl.oclc.org/ooxml/drawingml/main"
StrictNameSpaceExtendedProperties = "http://purl.oclc.org/ooxml/officeDocument/extendedProperties"
StrictNameSpaceSpreadSheet = "http://purl.oclc.org/ooxml/spreadsheetml/main"
StrictSourceRelationship = "http://purl.oclc.org/ooxml/officeDocument/relationships"
StrictSourceRelationshipChart = "http://purl.oclc.org/ooxml/officeDocument/relationships/chart"
StrictSourceRelationshipComments = "http://purl.oclc.org/ooxml/officeDocument/relationships/comments"
StrictSourceRelationshipExtendProperties = "http://purl.oclc.org/ooxml/officeDocument/relationships/extendedProperties"
StrictSourceRelationshipImage = "http://purl.oclc.org/ooxml/officeDocument/relationships/image"
StrictSourceRelationshipOfficeDocument = "http://purl.oclc.org/ooxml/officeDocument/relationships/officeDocument"
// The following constants defined the extLst child element
// ([ISO/IEC29500-1:2016] section 18.2.10) of the workbook and worksheet
// elements extended by the addition of new child ext elements.
ExtURICalcFeatures = "{B58B0392-4F1F-4190-BB64-5DF3571DCE5F}"
ExtURIConditionalFormattingRuleID = "{B025F937-C7B1-47D3-B67F-A62EFF666E3E}"
ExtURIConditionalFormattings = "{78C0D931-6437-407d-A8EE-F0AAD7539E65}"
ExtURIDataField = "{E15A36E0-9728-4E99-A89B-3F7291B0FE68}"
ExtURIDataModel = "{FCE2AD5D-F65C-4FA6-A056-5C36A1767C68}"
ExtURIDataValidations = "{CCE6A557-97BC-4b89-ADB6-D9C93CAAB3DF}"
ExtURIDrawingBlip = "{28A0092B-C50C-407E-A947-70E740481C1C}"
ExtURIExternalLinkPr = "{FCE6A71B-6B00-49CD-AB44-F6B1AE7CDE65}"
ExtURIIgnoredErrors = "{01252117-D84E-4E92-8308-4BE1C098FCBB}"
ExtURIMacExcelMX = "{64002731-A6B0-56B0-2670-7721B7C09600}"
ExtURIModelTimeGroupings = "{9835A34E-60A6-4A7C-AAB8-D5F71C897F49}"
ExtURIPivotCacheDefinition = "{725AE2AE-9491-48be-B2B4-4EB974FC3084}"
ExtURIPivotCachesX14 = "{876F7934-8845-4945-9796-88D515C7AA90}"
ExtURIPivotCachesX15 = "{841E416B-1EF1-43b6-AB56-02D37102CBD5}"
ExtURIPivotField = "{2946ED86-A175-432a-8AC1-64E0C546D7DE}"
ExtURIPivotFilter = "{0605FD5F-26C8-4aeb-8148-2DB25E43C511}"
ExtURIPivotHierarchy = "{F1805F06-0CD304483-9156-8803C3D141DF}"
ExtURIPivotTableReferences = "{983426D0-5260-488c-9760-48F4B6AC55F4}"
ExtURIProtectedRanges = "{FC87AEE6-9EDD-4A0A-B7FB-166176984837}"
ExtURISlicerCacheDefinition = "{2F2917AC-EB37-4324-AD4E-5DD8C200BD13}"
ExtURISlicerCacheHideItemsWithNoData = "{470722E0-AACD-4C17-9CDC-17EF765DBC7E}"
ExtURISlicerCachesX14 = "{BBE1A952-AA13-448e-AADC-164F8A28A991}"
ExtURISlicerCachesX15 = "{46BE6895-7355-4a93-B00E-2C351335B9C9}"
ExtURISlicerListX14 = "{A8765BA9-456A-4dab-B4F3-ACF838C121DE}"
ExtURISlicerListX15 = "{3A4CF648-6AED-40f4-86FF-DC5316D8AED3}"
ExtURISparklineGroups = "{05C60535-1F16-4fd2-B633-F4F36F0B64E0}"
ExtURISVG = "{96DAC541-7B7A-43D3-8B79-37D633B846F1}"
ExtURITimelineCachePivotCaches = "{A2CB5862-8E78-49c6-8D9D-AF26E26ADB89}"
ExtURITimelineCacheRefs = "{D0CA8CA8-9F24-4464-BF8E-62219DCF47F9}"
ExtURITimelineRefs = "{7E03D99C-DC04-49d9-9315-930204A7B6E9}"
ExtURIWebExtensions = "{F7C9EE02-42E1-4005-9D12-6889AFFD525C}"
ExtURIWorkbookPrX14 = "{79F54976-1DA5-4618-B147-ACDE4B953A38}"
ExtURIWorkbookPrX15 = "{140A7094-0E35-4892-8432-C4D2E57EDEB5}"
)
// workbookExtURIPriority is the priority of URI in the workbook extension lists.
var workbookExtURIPriority = []string{
ExtURIPivotCachesX14,
ExtURISlicerCachesX14,
ExtURISlicerCachesX15,
ExtURIWorkbookPrX14,
ExtURIPivotCachesX15,
ExtURIPivotTableReferences,
ExtURITimelineCachePivotCaches,
ExtURITimelineCacheRefs,
ExtURIWorkbookPrX15,
ExtURIDataModel,
ExtURICalcFeatures,
ExtURIExternalLinkPr,
ExtURIModelTimeGroupings,
}
// worksheetExtURIPriority is the priority of URI in the worksheet extension lists.
var worksheetExtURIPriority = []string{
ExtURIConditionalFormattings,
ExtURIDataValidations,
ExtURISparklineGroups,
ExtURISlicerListX14,
ExtURIProtectedRanges,
ExtURIIgnoredErrors,
ExtURIWebExtensions,
ExtURISlicerListX15,
ExtURITimelineRefs,
ExtURIExternalLinkPr,
}
// Excel specifications and limits
const (
MaxCellStyles = 65430
MaxColumns = 16384
MaxColumnWidth = 255
MaxFieldLength = 255
MaxFilePathLength = 207
MaxFormControlValue = 30000
MaxFontFamilyLength = 31
MaxFontSize = 409
MaxRowHeight = 409
MaxSheetNameLength = 31
MinColumns = 1
MinFontSize = 1
StreamChunkSize = 1 << 24
TotalCellChars = 32767
TotalRows = 1048576
TotalSheetHyperlinks = 65529
UnzipSizeLimit = 1000 << 24
// pivotTableVersion should be greater than 3. One or more of the
// PivotTables chosen are created in a version of Excel earlier than
// Excel 2007 or in compatibility mode. Slicer can only be used with
// PivotTables created in Excel 2007 or a newer version of Excel.
pivotTableVersion = 3
pivotTableRefreshedVersion = 8
defaultDrawingScale = 1.0
defaultChartDimensionWidth = 480
defaultChartDimensionHeight = 260
defaultSlicerWidth = 200
defaultSlicerHeight = 200
defaultChartLegendPosition = "bottom"
defaultChartShowBlanksAs = "gap"
defaultShapeSize = 160
defaultShapeLineWidth = 1
)
// ColorMappingType is the type of color transformation.
type ColorMappingType byte
// Color transformation types enumeration.
const (
ColorMappingTypeLight1 ColorMappingType = iota
ColorMappingTypeDark1
ColorMappingTypeLight2
ColorMappingTypeDark2
ColorMappingTypeAccent1
ColorMappingTypeAccent2
ColorMappingTypeAccent3
ColorMappingTypeAccent4
ColorMappingTypeAccent5
ColorMappingTypeAccent6
ColorMappingTypeHyperlink
ColorMappingTypeFollowedHyperlink
ColorMappingTypeUnset int = -1
)
// ChartDataLabelPositionType is the type of chart data labels position.
type ChartDataLabelPositionType byte
// Chart data labels positions types enumeration.
const (
ChartDataLabelsPositionUnset ChartDataLabelPositionType = iota
ChartDataLabelsPositionBestFit
ChartDataLabelsPositionBelow
ChartDataLabelsPositionCenter
ChartDataLabelsPositionInsideBase
ChartDataLabelsPositionInsideEnd
ChartDataLabelsPositionLeft
ChartDataLabelsPositionOutsideEnd
ChartDataLabelsPositionRight
ChartDataLabelsPositionAbove
)
// chartDataLabelsPositionTypes defined supported chart data labels position
// types.
var chartDataLabelsPositionTypes = map[ChartDataLabelPositionType]string{
ChartDataLabelsPositionBestFit: "bestFit",
ChartDataLabelsPositionBelow: "b",
ChartDataLabelsPositionCenter: "ctr",
ChartDataLabelsPositionInsideBase: "inBase",
ChartDataLabelsPositionInsideEnd: "inEnd",
ChartDataLabelsPositionLeft: "l",
ChartDataLabelsPositionOutsideEnd: "outEnd",
ChartDataLabelsPositionRight: "r",
ChartDataLabelsPositionAbove: "t",
}
// supportedChartDataLabelsPosition defined supported chart data labels position
// types for each type of chart.
var supportedChartDataLabelsPosition = map[ChartType][]ChartDataLabelPositionType{
Bar: {ChartDataLabelsPositionCenter, ChartDataLabelsPositionInsideBase, ChartDataLabelsPositionInsideEnd, ChartDataLabelsPositionOutsideEnd},
BarStacked: {ChartDataLabelsPositionCenter, ChartDataLabelsPositionInsideBase, ChartDataLabelsPositionInsideEnd},
BarPercentStacked: {ChartDataLabelsPositionCenter, ChartDataLabelsPositionInsideBase, ChartDataLabelsPositionInsideEnd},
Col: {ChartDataLabelsPositionCenter, ChartDataLabelsPositionInsideBase, ChartDataLabelsPositionInsideEnd, ChartDataLabelsPositionOutsideEnd},
ColStacked: {ChartDataLabelsPositionCenter, ChartDataLabelsPositionInsideBase, ChartDataLabelsPositionInsideEnd},
ColPercentStacked: {ChartDataLabelsPositionCenter, ChartDataLabelsPositionInsideBase, ChartDataLabelsPositionInsideEnd},
Line: {ChartDataLabelsPositionBelow, ChartDataLabelsPositionCenter, ChartDataLabelsPositionLeft, ChartDataLabelsPositionRight, ChartDataLabelsPositionAbove},
Pie: {ChartDataLabelsPositionBestFit, ChartDataLabelsPositionCenter, ChartDataLabelsPositionInsideEnd, ChartDataLabelsPositionOutsideEnd},
Pie3D: {ChartDataLabelsPositionBestFit, ChartDataLabelsPositionCenter, ChartDataLabelsPositionInsideEnd, ChartDataLabelsPositionOutsideEnd},
Scatter: {ChartDataLabelsPositionBelow, ChartDataLabelsPositionCenter, ChartDataLabelsPositionLeft, ChartDataLabelsPositionRight, ChartDataLabelsPositionAbove},
Bubble: {ChartDataLabelsPositionBelow, ChartDataLabelsPositionCenter, ChartDataLabelsPositionLeft, ChartDataLabelsPositionRight, ChartDataLabelsPositionAbove},
Bubble3D: {ChartDataLabelsPositionBelow, ChartDataLabelsPositionCenter, ChartDataLabelsPositionLeft, ChartDataLabelsPositionRight, ChartDataLabelsPositionAbove},
}
const (
defaultTempFileSST = "sharedStrings"
defaultXMLMetadata = "xl/metadata.xml"
defaultXMLPathCalcChain = "xl/calcChain.xml"
defaultXMLPathCellImages = "xl/cellimages.xml"
defaultXMLPathCellImagesRels = "xl/_rels/cellimages.xml.rels"
defaultXMLPathContentTypes = "[Content_Types].xml"
defaultXMLPathDocPropsApp = "docProps/app.xml"
defaultXMLPathDocPropsCore = "docProps/core.xml"
defaultXMLPathSharedStrings = "xl/sharedStrings.xml"
defaultXMLPathStyles = "xl/styles.xml"
defaultXMLPathTheme = "xl/theme/theme1.xml"
defaultXMLPathVolatileDeps = "xl/volatileDependencies.xml"
defaultXMLPathWorkbook = "xl/workbook.xml"
defaultXMLPathWorkbookRels = "xl/_rels/workbook.xml.rels"
defaultXMLRdRichValuePart = "xl/richData/rdrichvalue.xml"
defaultXMLRdRichValueRel = "xl/richData/richValueRel.xml"
defaultXMLRdRichValueRelRels = "xl/richData/_rels/richValueRel.xml.rels"
defaultXMLRdRichValueWebImagePart = "xl/richData/rdRichValueWebImage.xml"
defaultXMLRdRichValueWebImagePartRels = "xl/richData/_rels/rdRichValueWebImage.xml.rels"
)
// IndexedColorMapping is the table of default mappings from indexed color value
// to RGB value. Note that 0-7 are redundant of 8-15 to preserve backwards
// compatibility. A legacy indexing scheme for colors that is still required
// for some records, and for backwards compatibility with legacy formats. This
// element contains a sequence of RGB color values that correspond to color
// indexes (zero-based). When using the default indexed color palette, the
// values are not written out, but instead are implied. When the color palette
// has been modified from default, then the entire color palette is written
// out.
var IndexedColorMapping = []string{
"000000", "FFFFFF", "FF0000", "00FF00", "0000FF", "FFFF00", "FF00FF", "00FFFF",
"000000", "FFFFFF", "FF0000", "00FF00", "0000FF", "FFFF00", "FF00FF", "00FFFF",
"800000", "008000", "000080", "808000", "800080", "008080", "C0C0C0", "808080",
"9999FF", "993366", "FFFFCC", "CCFFFF", "660066", "FF8080", "0066CC", "CCCCFF",
"000080", "FF00FF", "FFFF00", "00FFFF", "800080", "800000", "008080", "0000FF",
"00CCFF", "CCFFFF", "CCFFCC", "FFFF99", "99CCFF", "FF99CC", "CC99FF", "FFCC99",
"3366FF", "33CCCC", "99CC00", "FFCC00", "FF9900", "FF6600", "666699", "969696",
"003366", "339966", "003300", "333300", "993300", "993366", "333399", "333333",
"000000", "FFFFFF",
}
// supportedDefinedNameAtStartCharCodeRange list the valid first character of a
// defined name ASCII letters.
var supportedDefinedNameAtStartCharCodeRange = []int{
65, 90, 92, 92, 95, 95, 97, 122, 161, 161, 164, 164,
167, 168, 170, 170, 173, 173, 175, 186, 188, 696, 699, 705,
711, 711, 713, 715, 717, 717, 720, 721, 728, 731, 733, 733,
736, 740, 750, 750, 880, 883, 886, 887, 890, 893, 902, 902,
904, 906, 908, 908, 910, 929, 931, 1013, 1015, 1153, 1162, 1315,
1329, 1366, 1369, 1369, 1377, 1415, 1488, 1514, 1520, 1522, 1569, 1610,
1646, 1647, 1649, 1747, 1749, 1749, 1765, 1766, 1774, 1775, 1786, 1788,
1791, 1791, 1808, 1808, 1810, 1839, 1869, 1957, 1969, 1969, 1994, 2026,
2036, 2037, 2042, 2042, 2308, 2361, 2365, 2365, 2384, 2384, 2392, 2401,
2417, 2418, 2427, 2431, 2437, 2444, 2447, 2448, 2451, 2472, 2474, 2480,
2482, 2482, 2486, 2489, 2493, 2493, 2510, 2510, 2524, 2525, 2527, 2529,
2544, 2545, 2565, 2570, 2575, 2576, 2579, 2600, 2602, 2608, 2610, 2611,
2613, 2614, 2616, 2617, 2649, 2652, 2654, 2654, 2674, 2676, 2693, 2701,
2703, 2705, 2707, 2728, 2730, 2736, 2738, 2739, 2741, 2745, 2749, 2749,
2768, 2768, 2784, 2785, 2821, 2828, 2831, 2832, 2835, 2856, 2858, 2864,
2866, 2867, 2869, 2873, 2877, 2877, 2908, 2909, 2911, 2913, 2929, 2929,
2947, 2947, 2949, 2954, 2958, 2960, 2962, 2965, 2969, 2970, 2972, 2972,
2974, 2975, 2979, 2980, 2984, 2986, 2990, 3001, 3024, 3024, 3077, 3084,
3086, 3088, 3090, 3112, 3114, 3123, 3125, 3129, 3133, 3133, 3160, 3161,
3168, 3169, 3205, 3212, 3214, 3216, 3218, 3240, 3242, 3251, 3253, 3257,
3261, 3261, 3294, 3294, 3296, 3297, 3333, 3340, 3342, 3344, 3346, 3368,
3370, 3385, 3389, 3389, 3424, 3425, 3450, 3455, 3461, 3478, 3482, 3505,
3507, 3515, 3517, 3517, 3520, 3526, 3585, 3642, 3648, 3662, 3713, 3714,
3716, 3716, 3719, 3720, 3722, 3722, 3725, 3725, 3732, 3735, 3737, 3743,
3745, 3747, 3749, 3749, 3751, 3751, 3754, 3755, 3757, 3760, 3762, 3763,
3773, 3773, 3776, 3780, 3782, 3782, 3804, 3805, 3840, 3840, 3904, 3911,
3913, 3948, 3976, 3979, 4096, 4138, 4159, 4159, 4176, 4181, 4186, 4189,
4193, 4193, 4197, 4198, 4206, 4208, 4213, 4225, 4238, 4238, 4256, 4293,
4304, 4346, 4348, 4348, 4352, 4441, 4447, 4514, 4520, 4601, 4608, 4680,
4682, 4685, 4688, 4694, 4696, 4696, 4698, 4701, 4704, 4744, 4746, 4749,
4752, 4784, 4786, 4789, 4792, 4798, 4800, 4800, 4802, 4805, 4808, 4822,
4824, 4880, 4882, 4885, 4888, 4954, 4992, 5007, 5024, 5108, 5121, 5740,
5743, 5750, 5761, 5786, 5792, 5866, 5870, 5872, 5888, 5900, 5902, 5905,
5920, 5937, 5952, 5969, 5984, 5996, 5998, 6000, 6016, 6067, 6103, 6103,
6108, 6108, 6176, 6263, 6272, 6312, 6314, 6314, 6400, 6428, 6480, 6509,
6512, 6516, 6528, 6569, 6593, 6599, 6656, 6678, 6917, 6963, 6981, 6987,
7043, 7072, 7086, 7087, 7168, 7203, 7245, 7247, 7258, 7293, 7424, 7615,
7680, 7957, 7960, 7965, 7968, 8005, 8008, 8013, 8016, 8023, 8025, 8025,
8027, 8027, 8029, 8029, 8031, 8061, 8064, 8116, 8118, 8124, 8126, 8126,
8130, 8132, 8134, 8140, 8144, 8147, 8150, 8155, 8160, 8172, 8178, 8180,
8182, 8188, 8208, 8208, 8211, 8214, 8216, 8216, 8220, 8221, 8224, 8225,
8229, 8231, 8240, 8240, 8242, 8243, 8245, 8245, 8251, 8251, 8305, 8305,
8308, 8308, 8319, 8319, 8321, 8324, 8336, 8340, 8450, 8451, 8453, 8453,
8455, 8455, 8457, 8467, 8469, 8470, 8473, 8477, 8481, 8482, 8484, 8484,
8486, 8486, 8488, 8488, 8490, 8493, 8495, 8505, 8508, 8511, 8517, 8521,
8526, 8526, 8531, 8532, 8539, 8542, 8544, 8584, 8592, 8601, 8658, 8658,
8660, 8660, 8704, 8704, 8706, 8707, 8711, 8712, 8715, 8715, 8719, 8719,
8721, 8721, 8725, 8725, 8730, 8730, 8733, 8736, 8739, 8739, 8741, 8741,
8743, 8748, 8750, 8750, 8756, 8759, 8764, 8765, 8776, 8776, 8780, 8780,
8786, 8786, 8800, 8801, 8804, 8807, 8810, 8811, 8814, 8815, 8834, 8835,
8838, 8839, 8853, 8853, 8857, 8857, 8869, 8869, 8895, 8895, 8978, 8978,
9312, 9397, 9424, 9449, 9472, 9547, 9552, 9588, 9601, 9615, 9618, 9621,
9632, 9633, 9635, 9641, 9650, 9651, 9654, 9655, 9660, 9661, 9664, 9665,
9670, 9672, 9675, 9675, 9678, 9681, 9698, 9701, 9711, 9711, 9733, 9734,
9737, 9737, 9742, 9743, 9756, 9756, 9758, 9758, 9792, 9792, 9794, 9794,
9824, 9825, 9827, 9829, 9831, 9834, 9836, 9837, 9839, 9839, 11264, 11310,
11312, 11358, 11360, 11375, 11377, 11389, 11392, 11492, 11520, 11557, 11568, 11621,
11631, 11631, 11648, 11670, 11680, 11686, 11688, 11694, 11696, 11702, 11704, 11710,
11712, 11718, 11720, 11726, 11728, 11734, 11736, 11742, 12288, 12291, 12293, 12311,
12317, 12319, 12321, 12329, 12337, 12341, 12344, 12348, 12353, 12438, 12443, 12447,
12449, 12543, 12549, 12589, 12593, 12686, 12704, 12727, 12784, 12828, 12832, 12841,
12849, 12850, 12857, 12857, 12896, 12923, 12927, 12927, 12963, 12968, 13059, 13059,
13069, 13069, 13076, 13076, 13080, 13080, 13090, 13091, 13094, 13095, 13099, 13099,
13110, 13110, 13115, 13115, 13129, 13130, 13133, 13133, 13137, 13137, 13143, 13143,
13179, 13182, 13184, 13188, 13192, 13258, 13261, 13267, 13269, 13270, 13272, 13272,
13275, 13277, 13312, 19893, 19968, 40899, 40960, 42124, 42240, 42508, 42512, 42527,
42538, 42539, 42560, 42591, 42594, 42606, 42624, 42647, 42786, 42887, 42891, 42892,
43003, 43009, 43011, 43013, 43015, 43018, 43020, 43042, 43072, 43123, 43138, 43187,
43274, 43301, 43312, 43334, 43520, 43560, 43584, 43586, 43588, 43595, 44032, 55203,
57344, 63560, 63744, 64045, 64048, 64106, 64112, 64217, 64256, 64262, 64275, 64279,
64285, 64285, 64287, 64296, 64298, 64310, 64312, 64316, 64318, 64318, 64320, 64321,
64323, 64324, 64326, 64433, 64467, 64829, 64848, 64911, 64914, 64967, 65008, 65019,
65072, 65073, 65075, 65092, 65097, 65106, 65108, 65111, 65113, 65126, 65128, 65131,
65136, 65140, 65142, 65276, 65281, 65374, 65377, 65470, 65474, 65479, 65482, 65487,
65490, 65495, 65498, 65500, 65504, 65510,
}
// supportedDefinedNameAfterStartCharCodeRange list the valid after first
// character of a defined name ASCII letters.
var supportedDefinedNameAfterStartCharCodeRange = []int{
46, 46, 48, 57, 63, 63, 65, 90, 92, 92, 95, 95,
97, 122, 161, 161, 164, 164, 167, 168, 170, 170, 173, 173,
175, 186, 188, 887, 890, 893, 900, 902, 904, 906, 908, 908,
910, 929, 931, 1315, 1329, 1366, 1369, 1369, 1377, 1415, 1425, 1469,
1471, 1471, 1473, 1474, 1476, 1477, 1479, 1479, 1488, 1514, 1520, 1522,
1536, 1539, 1542, 1544, 1547, 1547, 1550, 1562, 1567, 1567, 1569, 1630,
1632, 1641, 1646, 1747, 1749, 1791, 1807, 1866, 1869, 1969, 1984, 2038,
2042, 2042, 2305, 2361, 2364, 2381, 2384, 2388, 2392, 2403, 2406, 2415,
2417, 2418, 2427, 2431, 2433, 2435, 2437, 2444, 2447, 2448, 2451, 2472,
2474, 2480, 2482, 2482, 2486, 2489, 2492, 2500, 2503, 2504, 2507, 2510,
2519, 2519, 2524, 2525, 2527, 2531, 2534, 2554, 2561, 2563, 2565, 2570,
2575, 2576, 2579, 2600, 2602, 2608, 2610, 2611, 2613, 2614, 2616, 2617,
2620, 2620, 2622, 2626, 2631, 2632, 2635, 2637, 2641, 2641, 2649, 2652,
2654, 2654, 2662, 2677, 2689, 2691, 2693, 2701, 2703, 2705, 2707, 2728,
2730, 2736, 2738, 2739, 2741, 2745, 2748, 2757, 2759, 2761, 2763, 2765,
2768, 2768, 2784, 2787, 2790, 2799, 2801, 2801, 2817, 2819, 2821, 2828,
2831, 2832, 2835, 2856, 2858, 2864, 2866, 2867, 2869, 2873, 2876, 2884,
2887, 2888, 2891, 2893, 2902, 2903, 2908, 2909, 2911, 2915, 2918, 2929,
2946, 2947, 2949, 2954, 2958, 2960, 2962, 2965, 2969, 2970, 2972, 2972,
2974, 2975, 2979, 2980, 2984, 2986, 2990, 3001, 3006, 3010, 3014, 3016,
3018, 3021, 3024, 3024, 3031, 3031, 3046, 3066, 3073, 3075, 3077, 3084,
3086, 3088, 3090, 3112, 3114, 3123, 3125, 3129, 3133, 3140, 3142, 3144,
3146, 3149, 3157, 3158, 3160, 3161, 3168, 3171, 3174, 3183, 3192, 3199,
3202, 3203, 3205, 3212, 3214, 3216, 3218, 3240, 3242, 3251, 3253, 3257,
3260, 3268, 3270, 3272, 3274, 3277, 3285, 3286, 3294, 3294, 3296, 3299,
3302, 3311, 3313, 3314, 3330, 3331, 3333, 3340, 3342, 3344, 3346, 3368,
3370, 3385, 3389, 3396, 3398, 3400, 3402, 3405, 3415, 3415, 3424, 3427,
3430, 3445, 3449, 3455, 3458, 3459, 3461, 3478, 3482, 3505, 3507, 3515,
3517, 3517, 3520, 3526, 3530, 3530, 3535, 3540, 3542, 3542, 3544, 3551,
3570, 3571, 3585, 3642, 3647, 3662, 3664, 3673, 3713, 3714, 3716, 3716,
3719, 3720, 3722, 3722, 3725, 3725, 3732, 3735, 3737, 3743, 3745, 3747,
3749, 3749, 3751, 3751, 3754, 3755, 3757, 3769, 3771, 3773, 3776, 3780,
3782, 3782, 3784, 3789, 3792, 3801, 3804, 3805, 3840, 3843, 3859, 3897,
3902, 3911, 3913, 3948, 3953, 3972, 3974, 3979, 3984, 3991, 3993, 4028,
4030, 4044, 4046, 4047, 4096, 4169, 4176, 4249, 4254, 4293, 4304, 4346,
4348, 4348, 4352, 4441, 4447, 4514, 4520, 4601, 4608, 4680, 4682, 4685,
4688, 4694, 4696, 4696, 4698, 4701, 4704, 4744, 4746, 4749, 4752, 4784,
4786, 4789, 4792, 4798, 4800, 4800, 4802, 4805, 4808, 4822, 4824, 4880,
4882, 4885, 4888, 4954, 4959, 4960, 4969, 4988, 4992, 5017, 5024, 5108,
5121, 5740, 5743, 5750, 5760, 5786, 5792, 5866, 5870, 5872, 5888, 5900,
5902, 5908, 5920, 5940, 5952, 5971, 5984, 5996, 5998, 6000, 6002, 6003,
6016, 6099, 6103, 6103, 6107, 6109, 6112, 6121, 6128, 6137, 6155, 6158,
6160, 6169, 6176, 6263, 6272, 6314, 6400, 6428, 6432, 6443, 6448, 6459,
6464, 6464, 6470, 6509, 6512, 6516, 6528, 6569, 6576, 6601, 6608, 6617,
6624, 6683, 6912, 6987, 6992, 7001, 7009, 7036, 7040, 7082, 7086, 7097,
7168, 7223, 7232, 7241, 7245, 7293, 7424, 7654, 7678, 7957, 7960, 7965,
7968, 8005, 8008, 8013, 8016, 8023, 8025, 8025, 8027, 8027, 8029, 8029,
8031, 8061, 8064, 8116, 8118, 8132, 8134, 8147, 8150, 8155, 8157, 8175,
8178, 8180, 8182, 8190, 8192, 8208, 8211, 8214, 8216, 8216, 8220, 8221,
8224, 8225, 8229, 8240, 8242, 8243, 8245, 8245, 8251, 8251, 8260, 8260,
8274, 8274, 8287, 8292, 8298, 8305, 8308, 8316, 8319, 8332, 8336, 8340,
8352, 8373, 8400, 8432, 8448, 8527, 8531, 8584, 8592, 9000, 9003, 9191,
9216, 9254, 9280, 9290, 9312, 9885, 9888, 9916, 9920, 9923, 9985, 9988,
9990, 9993, 9996, 10023, 10025, 10059, 10061, 10061, 10063, 10066, 10070, 10070,
10072, 10078, 10081, 10087, 10102, 10132, 10136, 10159, 10161, 10174, 10176, 10180,
10183, 10186, 10188, 10188, 10192, 10213, 10224, 10626, 10649, 10711, 10716, 10747,
10750, 11084, 11088, 11092, 11264, 11310, 11312, 11358, 11360, 11375, 11377, 11389,
11392, 11498, 11517, 11517, 11520, 11557, 11568, 11621, 11631, 11631, 11648, 11670,
11680, 11686, 11688, 11694, 11696, 11702, 11704, 11710, 11712, 11718, 11720, 11726,
11728, 11734, 11736, 11742, 11744, 11775, 11823, 11823, 11904, 11929, 11931, 12019,
12032, 12245, 12272, 12283, 12288, 12311, 12317, 12335, 12337, 12348, 12350, 12351,
12353, 12438, 12441, 12447, 12449, 12543, 12549, 12589, 12593, 12686, 12688, 12727,
12736, 12771, 12784, 12830, 12832, 12867, 12880, 13054, 13056, 19893, 19904, 40899,
40960, 42124, 42128, 42182, 42240, 42508, 42512, 42539, 42560, 42591, 42594, 42610,
42620, 42621, 42623, 42647, 42752, 42892, 43003, 43051, 43072, 43123, 43136, 43204,
43216, 43225, 43264, 43310, 43312, 43347, 43520, 43574, 43584, 43597, 43600, 43609,
44032, 55203, 55296, 64045, 64048, 64106, 64112, 64217, 64256, 64262, 64275, 64279,
64285, 64310, 64312, 64316, 64318, 64318, 64320, 64321, 64323, 64324, 64326, 64433,
64467, 64829, 64848, 64911, 64914, 64967, 65008, 65021, 65024, 65039, 65056, 65062,
65072, 65073, 65075, 65092, 65097, 65106, 65108, 65111, 65113, 65126, 65128, 65131,
65136, 65140, 65142, 65276, 65279, 65279, 65281, 65374, 65377, 65470, 65474, 65479,
65482, 65487, 65490, 65495, 65498, 65500, 65504, 65510, 65512, 65518, 65529, 65533,
}
// supportedImageTypes defined supported image types.
var supportedImageTypes = map[string]string{
".bmp": ".bmp", ".emf": ".emf", ".emz": ".emz", ".gif": ".gif",
".jpeg": ".jpeg", ".jpg": ".jpeg", ".png": ".png", ".svg": ".svg",
".tif": ".tiff", ".tiff": ".tiff", ".wmf": ".wmf", ".wmz": ".wmz",
}
// supportedContentTypes defined supported file format types.
var supportedContentTypes = map[string]string{
".xlam": ContentTypeAddinMacro,
".xlsm": ContentTypeMacro,
".xlsx": ContentTypeSheetML,
".xltm": ContentTypeTemplateMacro,
".xltx": ContentTypeTemplate,
}
// supportedUnderlineTypes defined supported underline types.
var supportedUnderlineTypes = []string{"none", "single", "double"}
// supportedDrawingUnderlineTypes defined supported underline types in drawing
// markup language.
var supportedDrawingUnderlineTypes = []string{
"none", "words", "sng", "dbl", "heavy", "dotted", "dottedHeavy", "dash", "dashHeavy", "dashLong", "dashLongHeavy", "dotDash", "dotDashHeavy", "dotDotDash", "dotDotDashHeavy", "wavy", "wavyHeavy",
"wavyDbl",
}
// supportedDrawingTextVerticalType defined supported text vertical types in
// drawing markup language.
var supportedDrawingTextVerticalType = []string{"horz", "vert", "vert270", "wordArtVert", "eaVert", "mongolianVert", "wordArtVertRtl"}
// supportedPositioning defined supported positioning types.
var supportedPositioning = []string{"absolute", "oneCell", "twoCell"}
// supportedPageOrientation defined supported page setup page orientation.
var supportedPageOrientation = []string{"portrait", "landscape"}
// supportedPageOrder defined supported page setup page order.
var supportedPageOrder = []string{"overThenDown", "downThenOver"}
// builtInDefinedNames defined built-in defined names are built with a _xlnm prefix.
var builtInDefinedNames = []string{"_xlnm.Print_Area", "_xlnm.Print_Titles", "_xlnm.Criteria", "_xlnm._FilterDatabase", "_xlnm.Extract", "_xlnm.Consolidate_Area", "_xlnm.Database", "_xlnm.Sheet_Title"}
const templateDocpropsApp = `<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"><TotalTime>0</TotalTime><Application>Go Excelize</Application></Properties>` const templateDocpropsApp = `<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"><TotalTime>0</TotalTime><Application>Go Excelize</Application></Properties>`
const templateContentTypes = `<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Override PartName="/xl/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/><Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/><Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/><Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/><Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/></Types>` const templateContentTypes = `<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Override PartName="/xl/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/><Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/><Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/><Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/><Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/></Types>`

Binary file not shown.

1220
vml.go Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in // this source code is governed by a BSD-style license that can be found in
// the LICENSE file. // the LICENSE file.
// //
@ -7,7 +7,7 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming // Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of // API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.16 or later. // data. This library needs Go version 1.18 or later.
package excelize package excelize
@ -20,16 +20,16 @@ type vmlDrawing struct {
XMLNSv string `xml:"xmlns:v,attr"` XMLNSv string `xml:"xmlns:v,attr"`
XMLNSo string `xml:"xmlns:o,attr"` XMLNSo string `xml:"xmlns:o,attr"`
XMLNSx string `xml:"xmlns:x,attr"` XMLNSx string `xml:"xmlns:x,attr"`
XMLNSmv string `xml:"xmlns:mv,attr"` XMLNSmv string `xml:"xmlns:mv,attr,omitempty"`
Shapelayout *xlsxShapelayout `xml:"o:shapelayout"` ShapeLayout *xlsxShapeLayout `xml:"o:shapelayout"`
Shapetype *xlsxShapetype `xml:"v:shapetype"` ShapeType *xlsxShapeType `xml:"v:shapetype"`
Shape []xlsxShape `xml:"v:shape"` Shape []xlsxShape `xml:"v:shape"`
} }
// xlsxShapelayout directly maps the shapelayout element. This element contains // xlsxShapeLayout directly maps the shapelayout element. This element contains
// child elements that store information used in the editing and layout of // child elements that store information used in the editing and layout of
// shapes. // shapes.
type xlsxShapelayout struct { type xlsxShapeLayout struct {
Ext string `xml:"v:ext,attr"` Ext string `xml:"v:ext,attr"`
IDmap *xlsxIDmap `xml:"o:idmap"` IDmap *xlsxIDmap `xml:"o:idmap"`
} }
@ -44,33 +44,60 @@ type xlsxIDmap struct {
type xlsxShape struct { type xlsxShape struct {
XMLName xml.Name `xml:"v:shape"` XMLName xml.Name `xml:"v:shape"`
ID string `xml:"id,attr"` ID string `xml:"id,attr"`
SpID string `xml:"o:spid,attr,omitempty"`
Type string `xml:"type,attr"` Type string `xml:"type,attr"`
Style string `xml:"style,attr"` Style string `xml:"style,attr"`
Fillcolor string `xml:"fillcolor,attr"` Button string `xml:"o:button,attr,omitempty"`
Insetmode string `xml:"urn:schemas-microsoft-com:office:office insetmode,attr,omitempty"` Filled string `xml:"filled,attr,omitempty"`
Strokecolor string `xml:"strokecolor,attr,omitempty"` FillColor string `xml:"fillcolor,attr,omitempty"`
InsetMode string `xml:"urn:schemas-microsoft-com:office:office insetmode,attr,omitempty"`
Stroked string `xml:"stroked,attr,omitempty"`
StrokeColor string `xml:"strokecolor,attr,omitempty"`
Val string `xml:",innerxml"` Val string `xml:",innerxml"`
} }
// xlsxShapetype directly maps the shapetype element. // xlsxShapeType directly maps the shapetype element.
type xlsxShapetype struct { type xlsxShapeType struct {
ID string `xml:"id,attr"` ID string `xml:"id,attr"`
Coordsize string `xml:"coordsize,attr"` CoordSize string `xml:"coordsize,attr"`
Spt int `xml:"o:spt,attr"` Spt int `xml:"o:spt,attr"`
Path string `xml:"path,attr"` PreferRelative string `xml:"o:preferrelative,attr,omitempty"`
Stroke *xlsxStroke `xml:"v:stroke"` Path string `xml:"path,attr"`
VPath *vPath `xml:"v:path"` Filled string `xml:"filled,attr,omitempty"`
Stroked string `xml:"stroked,attr,omitempty"`
Stroke *xlsxStroke `xml:"v:stroke"`
VFormulas *vFormulas `xml:"v:formulas"`
VPath *vPath `xml:"v:path"`
Lock *oLock `xml:"o:lock"`
} }
// xlsxStroke directly maps the stroke element. // xlsxStroke directly maps the stroke element.
type xlsxStroke struct { type xlsxStroke struct {
Joinstyle string `xml:"joinstyle,attr"` JoinStyle string `xml:"joinstyle,attr"`
} }
// vPath directly maps the v:path element. // vPath directly maps the v:path element.
type vPath struct { type vPath struct {
Gradientshapeok string `xml:"gradientshapeok,attr,omitempty"` ExtrusionOK string `xml:"o:extrusionok,attr,omitempty"`
Connecttype string `xml:"o:connecttype,attr"` GradientShapeOK string `xml:"gradientshapeok,attr,omitempty"`
ConnectType string `xml:"o:connecttype,attr"`
}
// oLock directly maps the o:lock element.
type oLock struct {
Ext string `xml:"v:ext,attr"`
Rotation string `xml:"rotation,attr,omitempty"`
AspectRatio string `xml:"aspectratio,attr,omitempty"`
}
// vFormulas directly maps to the v:formulas element
type vFormulas struct {
Formulas []vFormula `xml:"v:f"`
}
// vFormula directly maps to the v:f element
type vFormula struct {
Equation string `xml:"eqn,attr"`
} }
// vFill directly maps the v:fill element. This element must be defined within a // vFill directly maps the v:fill element. This element must be defined within a
@ -96,16 +123,31 @@ type vShadow struct {
Obscured string `xml:"obscured,attr"` Obscured string `xml:"obscured,attr"`
} }
// vTextbox directly maps the v:textbox element. This element must be defined // vTextBox directly maps the v:textbox element. This element must be defined
// within a Shape element. // within a Shape element.
type vTextbox struct { type vTextBox struct {
Style string `xml:"style,attr"` Style string `xml:"style,attr"`
Div *xlsxDiv `xml:"div"` Div *xlsxDiv `xml:"div"`
} }
// vImageData directly maps the v:imagedata element. This element must be
// defined within a Shape element.
type vImageData struct {
RelID string `xml:"o:relid,attr"`
Title string `xml:"o:title,attr,omitempty"`
}
// xlsxDiv directly maps the div element. // xlsxDiv directly maps the div element.
type xlsxDiv struct { type xlsxDiv struct {
Style string `xml:"style,attr"` Style string `xml:"style,attr"`
Font []vmlFont `xml:"font"`
}
type vmlFont struct {
Face string `xml:"face,attr,omitempty"`
Size uint `xml:"size,attr,omitempty"`
Color string `xml:"color,attr,omitempty"`
Content string `xml:",innerxml"`
} }
// xClientData (Attached Object Data) directly maps the x:ClientData element. // xClientData (Attached Object Data) directly maps the x:ClientData element.
@ -116,24 +158,129 @@ type xlsxDiv struct {
// child elements is appropriate. Relevant groups are identified for each child // child elements is appropriate. Relevant groups are identified for each child
// element. // element.
type xClientData struct { type xClientData struct {
ObjectType string `xml:"ObjectType,attr"` ObjectType string `xml:"ObjectType,attr"`
MoveWithCells string `xml:"x:MoveWithCells,omitempty"` MoveWithCells *string `xml:"x:MoveWithCells"`
SizeWithCells string `xml:"x:SizeWithCells,omitempty"` SizeWithCells *string `xml:"x:SizeWithCells"`
Anchor string `xml:"x:Anchor"` Anchor string `xml:"x:Anchor"`
AutoFill string `xml:"x:AutoFill"` Locked string `xml:"x:Locked,omitempty"`
Row int `xml:"x:Row"` PrintObject string `xml:"x:PrintObject,omitempty"`
Column int `xml:"x:Column"` AutoFill string `xml:"x:AutoFill,omitempty"`
FmlaMacro string `xml:"x:FmlaMacro,omitempty"`
TextHAlign string `xml:"x:TextHAlign,omitempty"`
TextVAlign string `xml:"x:TextVAlign,omitempty"`
Row *int `xml:"x:Row"`
Column *int `xml:"x:Column"`
Checked int `xml:"x:Checked,omitempty"`
FmlaLink string `xml:"x:FmlaLink,omitempty"`
NoThreeD *string `xml:"x:NoThreeD"`
FirstButton *string `xml:"x:FirstButton"`
Val uint `xml:"x:Val,omitempty"`
Min uint `xml:"x:Min,omitempty"`
Max uint `xml:"x:Max,omitempty"`
Inc uint `xml:"x:Inc,omitempty"`
Page uint `xml:"x:Page,omitempty"`
Horiz *string `xml:"x:Horiz"`
Dx uint `xml:"x:Dx,omitempty"`
} }
// decodeVmlDrawing defines the structure used to parse the file // decodeVmlDrawing defines the structure used to parse the file
// xl/drawings/vmlDrawing%d.vml. // xl/drawings/vmlDrawing%d.vml.
type decodeVmlDrawing struct { type decodeVmlDrawing struct {
Shape []decodeShape `xml:"urn:schemas-microsoft-com:vml shape"` ShapeType decodeShapeType `xml:"urn:schemas-microsoft-com:vml shapetype"`
Shape []decodeShape `xml:"urn:schemas-microsoft-com:vml shape"`
}
// decodeShapeType defines the structure used to parse the shapetype element in
// the file xl/drawings/vmlDrawing%d.vml.
type decodeShapeType struct {
ID string `xml:"id,attr"`
CoordSize string `xml:"coordsize,attr"`
Spt int `xml:"spt,attr"`
PreferRelative string `xml:"preferrelative,attr,omitempty"`
Path string `xml:"path,attr"`
Filled string `xml:"filled,attr,omitempty"`
Stroked string `xml:"stroked,attr,omitempty"`
} }
// decodeShape defines the structure used to parse the particular shape element. // decodeShape defines the structure used to parse the particular shape element.
type decodeShape struct { type decodeShape struct {
Val string `xml:",innerxml"` ID string `xml:"id,attr"`
SpID string `xml:"spid,attr,omitempty"`
Type string `xml:"type,attr"`
Style string `xml:"style,attr"`
Button string `xml:"button,attr,omitempty"`
Filled string `xml:"filled,attr,omitempty"`
FillColor string `xml:"fillcolor,attr,omitempty"`
InsetMode string `xml:"urn:schemas-microsoft-com:office:office insetmode,attr,omitempty"`
Stroked string `xml:"stroked,attr,omitempty"`
StrokeColor string `xml:"strokecolor,attr,omitempty"`
Val string `xml:",innerxml"`
}
// decodeShapeVal defines the structure used to parse the sub-element of the
// shape in the file xl/drawings/vmlDrawing%d.vml.
type decodeShapeVal struct {
TextBox decodeVMLTextBox `xml:"textbox"`
ClientData decodeVMLClientData `xml:"ClientData"`
}
// decodeVMLFontU defines the structure used to parse the u element in the VML.
type decodeVMLFontU struct {
Class string `xml:"class,attr"`
Val string `xml:",chardata"`
}
// decodeVMLFontI defines the structure used to parse the i element in the VML.
type decodeVMLFontI struct {
U *decodeVMLFontU `xml:"u"`
Val string `xml:",chardata"`
}
// decodeVMLFontB defines the structure used to parse the b element in the VML.
type decodeVMLFontB struct {
I *decodeVMLFontI `xml:"i"`
U *decodeVMLFontU `xml:"u"`
Val string `xml:",chardata"`
}
// decodeVMLFont defines the structure used to parse the font element in the VML.
type decodeVMLFont struct {
Face string `xml:"face,attr,omitempty"`
Size uint `xml:"size,attr,omitempty"`
Color string `xml:"color,attr,omitempty"`
B *decodeVMLFontB `xml:"b"`
I *decodeVMLFontI `xml:"i"`
U *decodeVMLFontU `xml:"u"`
Val string `xml:",chardata"`
}
// decodeVMLDiv defines the structure used to parse the div element in the VML.
type decodeVMLDiv struct {
Font []decodeVMLFont `xml:"font"`
}
// decodeVMLTextBox defines the structure used to parse the v:textbox element in
// the file xl/drawings/vmlDrawing%d.vml.
type decodeVMLTextBox struct {
Div decodeVMLDiv `xml:"div"`
}
// decodeVMLClientData defines the structure used to parse the x:ClientData
// element in the file xl/drawings/vmlDrawing%d.vml.
type decodeVMLClientData struct {
ObjectType string `xml:"ObjectType,attr"`
Anchor string
FmlaMacro string
Column *int
Row *int
Checked int
FmlaLink string
Val uint
Min uint
Max uint
Inc uint
Page uint
Horiz *string
} }
// encodeShape defines the structure used to re-serialization shape element. // encodeShape defines the structure used to re-serialization shape element.
@ -141,6 +288,65 @@ type encodeShape struct {
Fill *vFill `xml:"v:fill"` Fill *vFill `xml:"v:fill"`
Shadow *vShadow `xml:"v:shadow"` Shadow *vShadow `xml:"v:shadow"`
Path *vPath `xml:"v:path"` Path *vPath `xml:"v:path"`
Textbox *vTextbox `xml:"v:textbox"` TextBox *vTextBox `xml:"v:textbox"`
ImageData *vImageData `xml:"v:imagedata"`
ClientData *xClientData `xml:"x:ClientData"` ClientData *xClientData `xml:"x:ClientData"`
Lock *oLock `xml:"o:lock"`
}
// formCtrlPreset defines the structure used to form control presets.
type formCtrlPreset struct {
autoFill string
fill *vFill
fillColor string
filled string
firstButton *string
noThreeD *string
objectType string
shadow *vShadow
strokeButton string
strokeColor string
stroked string
textHAlign string
textVAlign string
}
// vmlOptions defines the structure used to internal comments and form controls.
type vmlOptions struct {
formCtrl bool
sheet string
Comment
FormControl
}
// FormControl directly maps the form controls information.
type FormControl struct {
Cell string
Macro string
Width uint
Height uint
Checked bool
CurrentVal uint
MinVal uint
MaxVal uint
IncChange uint
PageChange uint
Horizontally bool
CellLink string
Text string
Paragraph []RichTextRun
Type FormControlType
Format GraphicOptions
}
// HeaderFooterImageOptions defines the settings for an image to be accessible
// from the worksheet header and footer options.
type HeaderFooterImageOptions struct {
Position HeaderFooterImagePositionType
File []byte
IsFooter bool
FirstPage bool
Extension string
Width string
Height string
} }

511
vml_test.go Normal file
View File

@ -0,0 +1,511 @@
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
// Package excelize providing a set of functions that allow you to write to and
// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.18 or later.
package excelize
import (
"encoding/xml"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestAddComment(t *testing.T) {
f, err := prepareTestBook1()
if !assert.NoError(t, err) {
t.FailNow()
}
s := strings.Repeat("c", TotalCellChars+1)
assert.NoError(t, f.AddComment("Sheet1", Comment{Cell: "A30", Author: s, Text: s, Paragraph: []RichTextRun{{Text: s}, {Text: s}}}))
assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "B7", Author: "Excelize", Text: s[:TotalCellChars-1], Paragraph: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment."}}}))
// Test add comment on not exists worksheet
assert.EqualError(t, f.AddComment("SheetN", Comment{Cell: "B7", Author: "Excelize", Paragraph: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment."}}}), "sheet SheetN does not exist")
// Test add comment on with illegal cell reference
assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.AddComment("Sheet1", Comment{Cell: "A", Author: "Excelize", Paragraph: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment."}}}))
comments, err := f.GetComments("Sheet1")
assert.NoError(t, err)
assert.Len(t, comments, 2)
comments, err = f.GetComments("Sheet2")
assert.NoError(t, err)
assert.Len(t, comments, 1)
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddComments.xlsx")))
f.Comments["xl/comments2.xml"] = nil
f.Pkg.Store("xl/comments2.xml", []byte(xml.Header+`<comments xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><authors><author>Excelize: </author></authors><commentList><comment ref="B7" authorId="0"><text><t>Excelize: </t></text></comment></commentList></comments>`))
comments, err = f.GetComments("Sheet1")
assert.NoError(t, err)
assert.Len(t, comments, 2)
comments, err = f.GetComments("Sheet2")
assert.NoError(t, err)
assert.Len(t, comments, 1)
comments, err = NewFile().GetComments("Sheet1")
assert.NoError(t, err)
assert.Len(t, comments, 0)
// Test add comments with invalid sheet name
assert.Equal(t, ErrSheetNameInvalid, f.AddComment("Sheet:1", Comment{Cell: "A1", Author: "Excelize", Text: "This is a comment."}))
// Test add comments with unsupported charset
f.Comments["xl/comments2.xml"] = nil
f.Pkg.Store("xl/comments2.xml", MacintoshCyrillicCharset)
_, err = f.GetComments("Sheet2")
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
// Test add comments with unsupported charset
f.Comments["xl/comments2.xml"] = nil
f.Pkg.Store("xl/comments2.xml", MacintoshCyrillicCharset)
assert.EqualError(t, f.AddComment("Sheet2", Comment{Cell: "A30", Text: "Comment"}), "XML syntax error on line 1: invalid UTF-8")
// Test add comments with unsupported charset style sheet
f.Styles = nil
f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset)
assert.EqualError(t, f.AddComment("Sheet2", Comment{Cell: "A30", Text: "Comment"}), "XML syntax error on line 1: invalid UTF-8")
// Test get comments on not exists worksheet
comments, err = f.GetComments("SheetN")
assert.Len(t, comments, 0)
assert.EqualError(t, err, "sheet SheetN does not exist")
}
func TestDeleteComment(t *testing.T) {
f, err := prepareTestBook1()
if !assert.NoError(t, err) {
t.FailNow()
}
assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "A40", Text: "Excelize: This is a comment1."}))
assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "A41", Paragraph: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment2."}}}))
assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "C41", Paragraph: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment3."}}}))
assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "C41", Paragraph: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment3-1."}}}))
assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "C42", Paragraph: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment4."}}}))
assert.NoError(t, f.AddComment("Sheet2", Comment{Cell: "C41", Paragraph: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment2."}}}))
assert.NoError(t, f.DeleteComment("Sheet2", "A40"))
comments, err := f.GetComments("Sheet2")
assert.NoError(t, err)
assert.Len(t, comments, 5)
comments, err = NewFile().GetComments("Sheet1")
assert.NoError(t, err)
assert.Len(t, comments, 0)
// Test delete comment with invalid sheet name
assert.Equal(t, ErrSheetNameInvalid, f.DeleteComment("Sheet:1", "A1"))
// Test delete all comments in a worksheet
assert.NoError(t, f.DeleteComment("Sheet2", "A41"))
assert.NoError(t, f.DeleteComment("Sheet2", "C41"))
assert.NoError(t, f.DeleteComment("Sheet2", "C42"))
comments, err = f.GetComments("Sheet2")
assert.NoError(t, err)
assert.EqualValues(t, 0, len(comments))
// Test delete comment on not exists worksheet
assert.EqualError(t, f.DeleteComment("SheetN", "A1"), "sheet SheetN does not exist")
// Test delete comment with worksheet part
f.Pkg.Delete("xl/worksheets/sheet1.xml")
assert.NoError(t, f.DeleteComment("Sheet1", "A22"))
f.Comments["xl/comments2.xml"] = nil
f.Pkg.Store("xl/comments2.xml", MacintoshCyrillicCharset)
assert.EqualError(t, f.DeleteComment("Sheet2", "A41"), "XML syntax error on line 1: invalid UTF-8")
f = NewFile()
// Test delete comment on a no comments worksheet
assert.NoError(t, f.DeleteComment("Sheet1", "A1"))
}
func TestDecodeVMLDrawingReader(t *testing.T) {
f := NewFile()
path := "xl/drawings/vmlDrawing1.xml"
f.Pkg.Store(path, MacintoshCyrillicCharset)
_, err := f.decodeVMLDrawingReader(path)
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
}
func TestCommentsReader(t *testing.T) {
f := NewFile()
// Test read comments with unsupported charset
path := "xl/comments1.xml"
f.Pkg.Store(path, MacintoshCyrillicCharset)
_, err := f.commentsReader(path)
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
}
func TestCountComments(t *testing.T) {
f := NewFile()
f.Comments["xl/comments1.xml"] = nil
assert.Equal(t, f.countComments(), 1)
}
func TestAddDrawingVML(t *testing.T) {
// Test addDrawingVML with illegal cell reference
f := NewFile()
assert.Equal(t, f.addDrawingVML(0, "", &vmlOptions{FormControl: FormControl{Cell: "*"}}), newCellNameToCoordinatesError("*", newInvalidCellNameError("*")))
f.Pkg.Store("xl/drawings/vmlDrawing1.vml", MacintoshCyrillicCharset)
assert.EqualError(t, f.addDrawingVML(0, "xl/drawings/vmlDrawing1.vml", &vmlOptions{sheet: "Sheet1", FormControl: FormControl{Cell: "A1"}}), "XML syntax error on line 1: invalid UTF-8")
}
func TestFormControl(t *testing.T) {
f := NewFile()
formControls := []FormControl{
{
Cell: "D1", Type: FormControlButton, Macro: "Button1_Click",
},
{
Cell: "A1", Type: FormControlButton, Macro: "Button1_Click",
Width: 140, Height: 60, Text: "Button 1\n",
Paragraph: []RichTextRun{
{
Font: &Font{
Bold: true,
Italic: true,
Underline: "single",
Family: "Times New Roman",
Size: 14,
Color: "777777",
},
Text: "C1=A1+B1",
},
},
Format: GraphicOptions{PrintObject: boolPtr(true), Positioning: "absolute"},
},
{
Cell: "A5", Type: FormControlCheckBox, Text: "Check Box 1",
Checked: true, Format: GraphicOptions{
PrintObject: boolPtr(false), Positioning: "oneCell",
},
},
{
Cell: "A6", Type: FormControlCheckBox, Text: "Check Box 2",
Format: GraphicOptions{Positioning: "twoCell"},
},
{
Cell: "A7", Type: FormControlOptionButton, Text: "Option Button 1", Checked: true,
},
{
Cell: "A8", Type: FormControlOptionButton, Text: "Option Button 2",
},
{
Cell: "D3", Type: FormControlGroupBox, Text: "Group Box 1",
Width: 140, Height: 60,
},
{
Cell: "A9", Type: FormControlLabel, Text: "Label 1", Width: 140,
},
{
Cell: "C5", Type: FormControlSpinButton, Width: 40, Height: 60,
CurrentVal: 7, MinVal: 5, MaxVal: 10, IncChange: 1, CellLink: "C2",
},
{
Cell: "D7", Type: FormControlScrollBar, Width: 140, Height: 20,
CurrentVal: 50, MinVal: 10, MaxVal: 100, IncChange: 1, PageChange: 1, Horizontally: true, CellLink: "C3",
},
{
Cell: "G1", Type: FormControlScrollBar, Width: 20, Height: 140,
CurrentVal: 50, MinVal: 1000, MaxVal: 100, IncChange: 1, PageChange: 1, CellLink: "C4",
},
}
for _, formCtrl := range formControls {
assert.NoError(t, f.AddFormControl("Sheet1", formCtrl))
}
// Test get from controls
result, err := f.GetFormControls("Sheet1")
assert.NoError(t, err)
assert.Len(t, result, 11)
for i, formCtrl := range formControls {
assert.Equal(t, formCtrl.Type, result[i].Type)
assert.Equal(t, formCtrl.Cell, result[i].Cell)
assert.Equal(t, formCtrl.Macro, result[i].Macro)
assert.Equal(t, formCtrl.Checked, result[i].Checked)
assert.Equal(t, formCtrl.CurrentVal, result[i].CurrentVal)
assert.Equal(t, formCtrl.MinVal, result[i].MinVal)
assert.Equal(t, formCtrl.MaxVal, result[i].MaxVal)
assert.Equal(t, formCtrl.IncChange, result[i].IncChange)
assert.Equal(t, formCtrl.Horizontally, result[i].Horizontally)
assert.Equal(t, formCtrl.CellLink, result[i].CellLink)
assert.Equal(t, formCtrl.Text, result[i].Text)
assert.Equal(t, len(formCtrl.Paragraph), len(result[i].Paragraph))
}
assert.NoError(t, f.SetSheetProps("Sheet1", &SheetPropsOptions{CodeName: stringPtr("Sheet1")}))
file, err := os.ReadFile(filepath.Join("test", "vbaProject.bin"))
assert.NoError(t, err)
assert.NoError(t, f.AddVBAProject(file))
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddFormControl.xlsm")))
assert.NoError(t, f.Close())
f, err = OpenFile(filepath.Join("test", "TestAddFormControl.xlsm"))
assert.NoError(t, err)
// Test get from controls before add form controls
result, err = f.GetFormControls("Sheet1")
assert.NoError(t, err)
assert.Len(t, result, 11)
// Test add from control to a worksheet which already contains form controls
assert.NoError(t, f.AddFormControl("Sheet1", FormControl{
Cell: "D4", Type: FormControlButton, Macro: "Button1_Click",
Paragraph: []RichTextRun{{Font: &Font{Underline: "double"}, Text: "Button 2"}},
}))
// Test get from controls after add form controls
result, err = f.GetFormControls("Sheet1")
assert.NoError(t, err)
assert.Len(t, result, 12)
// Test add unsupported form control
assert.Equal(t, f.AddFormControl("Sheet1", FormControl{
Cell: "A1", Type: 0x37, Macro: "Button1_Click",
}), ErrParameterInvalid)
// Test add form control on not exists worksheet
assert.Equal(t, ErrSheetNotExist{"SheetN"}, f.AddFormControl("SheetN", FormControl{
Cell: "A1", Type: FormControlButton, Macro: "Button1_Click",
}))
// Test add form control with invalid positioning types
assert.Equal(t, f.AddFormControl("Sheet1", FormControl{
Cell: "A1", Type: FormControlButton,
Format: GraphicOptions{Positioning: "x"},
}), ErrParameterInvalid)
// Test add spin form control with illegal cell link reference
assert.Equal(t, f.AddFormControl("Sheet1", FormControl{
Cell: "C5", Type: FormControlSpinButton, CellLink: "*",
}), newCellNameToCoordinatesError("*", newInvalidCellNameError("*")))
// Test add spin form control with invalid scroll value
assert.Equal(t, f.AddFormControl("Sheet1", FormControl{
Cell: "C5", Type: FormControlSpinButton, CurrentVal: MaxFormControlValue + 1,
}), ErrFormControlValue)
assert.NoError(t, f.Close())
// Test delete form control
f, err = OpenFile(filepath.Join("test", "TestAddFormControl.xlsm"))
assert.NoError(t, err)
assert.NoError(t, f.DeleteFormControl("Sheet1", "D1"))
assert.NoError(t, f.DeleteFormControl("Sheet1", "A1"))
// Test get from controls after delete form controls
result, err = f.GetFormControls("Sheet1")
assert.NoError(t, err)
assert.Len(t, result, 9)
// Test delete form control on not exists worksheet
assert.Equal(t, ErrSheetNotExist{"SheetN"}, f.DeleteFormControl("SheetN", "A1"))
// Test delete form control with illegal cell link reference
assert.Equal(t, f.DeleteFormControl("Sheet1", "A"), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")))
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDeleteFormControl.xlsm")))
assert.NoError(t, f.Close())
// Test delete form control with expected element
f, err = OpenFile(filepath.Join("test", "TestAddFormControl.xlsm"))
assert.NoError(t, err)
f.Pkg.Store("xl/drawings/vmlDrawing1.vml", MacintoshCyrillicCharset)
assert.Error(t, f.DeleteFormControl("Sheet1", "A1"), "XML syntax error on line 1: invalid UTF-8")
// Test delete form controls with invalid shape anchor
f.DecodeVMLDrawing["xl/drawings/vmlDrawing1.vml"] = &decodeVmlDrawing{
Shape: []decodeShape{{Type: "#_x0000_t201", Val: "<x:ClientData ObjectType=\"Scroll\"><x:Anchor>0</x:Anchor></x:ClientData>"}},
}
assert.Equal(t, ErrParameterInvalid, f.DeleteFormControl("Sheet1", "A1"))
assert.NoError(t, f.Close())
// Test delete form control on a worksheet without form control
f = NewFile()
assert.NoError(t, f.DeleteFormControl("Sheet1", "A1"))
// Test get form controls on a worksheet without form control
_, err = f.GetFormControls("Sheet1")
assert.NoError(t, err)
// Test get form controls on not exists worksheet
_, err = f.GetFormControls("SheetN")
assert.Equal(t, ErrSheetNotExist{"SheetN"}, err)
// Test get form controls with unsupported charset VML drawing
f, err = OpenFile(filepath.Join("test", "TestAddFormControl.xlsm"))
assert.NoError(t, err)
f.Pkg.Store("xl/drawings/vmlDrawing1.vml", MacintoshCyrillicCharset)
_, err = f.GetFormControls("Sheet1")
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
// Test get form controls with unsupported shape type
f.DecodeVMLDrawing["xl/drawings/vmlDrawing1.vml"] = &decodeVmlDrawing{
Shape: []decodeShape{{Type: "_x0000_t202"}},
}
formControls, err = f.GetFormControls("Sheet1")
assert.NoError(t, err)
assert.Len(t, formControls, 0)
// Test get form controls with bold font format
f.DecodeVMLDrawing["xl/drawings/vmlDrawing1.vml"] = &decodeVmlDrawing{
Shape: []decodeShape{{Type: "#_x0000_t201", Val: "<v:textbox><div><font><b>Text</b></font></div></v:textbox><x:ClientData ObjectType=\"Scroll\"><x:Anchor>0,0,0,0,0,0,0,0</x:Anchor></x:ClientData>"}},
}
formControls, err = f.GetFormControls("Sheet1")
assert.NoError(t, err)
assert.True(t, formControls[0].Paragraph[0].Font.Bold)
// Test get form controls with italic font format
f.DecodeVMLDrawing["xl/drawings/vmlDrawing1.vml"] = &decodeVmlDrawing{
Shape: []decodeShape{{Type: "#_x0000_t201", Val: "<v:textbox><div><font><i>Text</i></font></div></v:textbox><x:ClientData ObjectType=\"Scroll\"><x:Anchor>0,0,0,0,0,0,0,0</x:Anchor></x:ClientData>"}},
}
formControls, err = f.GetFormControls("Sheet1")
assert.NoError(t, err)
assert.True(t, formControls[0].Paragraph[0].Font.Italic)
// Test get form controls with font format
f.DecodeVMLDrawing["xl/drawings/vmlDrawing1.vml"] = &decodeVmlDrawing{
Shape: []decodeShape{{Type: "#_x0000_t201", Val: "<v:textbox><div><font face=\"Calibri\" size=\"280\" color=\"#777777\">Text</font></div></v:textbox><x:ClientData ObjectType=\"Scroll\"><x:Anchor>0,0,0,0,0,0,0,0</x:Anchor></x:ClientData>"}},
}
formControls, err = f.GetFormControls("Sheet1")
assert.NoError(t, err)
assert.Equal(t, "Calibri", formControls[0].Paragraph[0].Font.Family)
assert.Equal(t, 14.0, formControls[0].Paragraph[0].Font.Size)
assert.Equal(t, "#777777", formControls[0].Paragraph[0].Font.Color)
// Test get form controls with italic font format
f.DecodeVMLDrawing["xl/drawings/vmlDrawing1.vml"] = &decodeVmlDrawing{
Shape: []decodeShape{{Type: "#_x0000_t201", Val: "<v:textbox><div><font><i>Text</i></font></div></v:textbox><x:ClientData ObjectType=\"Scroll\"><x:Anchor>0,0,0,0,0,0,0,0</x:Anchor></x:ClientData>"}},
}
formControls, err = f.GetFormControls("Sheet1")
assert.NoError(t, err)
assert.True(t, formControls[0].Paragraph[0].Font.Italic)
// Test get form controls with invalid column number
f.DecodeVMLDrawing["xl/drawings/vmlDrawing1.vml"] = &decodeVmlDrawing{
Shape: []decodeShape{{Type: "#_x0000_t201", Val: fmt.Sprintf("<x:ClientData ObjectType=\"Scroll\"><x:Anchor>%d,0,0,0,0,0,0,0</x:Anchor></x:ClientData>", MaxColumns)}},
}
formControls, err = f.GetFormControls("Sheet1")
assert.Equal(t, err, ErrColumnNumber)
assert.Len(t, formControls, 0)
// Test get form controls with comment (Note) shape type
f.DecodeVMLDrawing["xl/drawings/vmlDrawing1.vml"] = &decodeVmlDrawing{
Shape: []decodeShape{{Type: "#_x0000_t201", Val: "<x:ClientData ObjectType=\"Note\"></x:ClientData>"}},
}
formControls, err = f.GetFormControls("Sheet1")
assert.NoError(t, err)
assert.Len(t, formControls, 0)
// Test get form controls with unsupported shape type
f.VMLDrawing["xl/drawings/vmlDrawing1.vml"] = &vmlDrawing{
Shape: []xlsxShape{{Type: "_x0000_t202"}},
}
formControls, err = f.GetFormControls("Sheet1")
assert.NoError(t, err)
assert.Len(t, formControls, 0)
// Test get form controls with invalid column number
f.VMLDrawing["xl/drawings/vmlDrawing1.vml"] = &vmlDrawing{
Shape: []xlsxShape{{Type: "#_x0000_t201", Val: fmt.Sprintf("<x:ClientData ObjectType=\"Scroll\"><x:Anchor>%d,0,0,0,0,0,0,0</x:Anchor></x:ClientData>", MaxColumns)}},
}
formControls, err = f.GetFormControls("Sheet1")
assert.Equal(t, err, ErrColumnNumber)
assert.Len(t, formControls, 0)
// Test get form controls with invalid shape anchor
f.VMLDrawing["xl/drawings/vmlDrawing1.vml"] = &vmlDrawing{
Shape: []xlsxShape{{Type: "#_x0000_t201", Val: "<x:ClientData ObjectType=\"Scroll\"><x:Anchor>x,0,0,0,0,0,0,0</x:Anchor></x:ClientData>"}},
}
formControls, err = f.GetFormControls("Sheet1")
assert.Equal(t, ErrColumnNumber, err)
assert.Len(t, formControls, 0)
// Test get form controls with comment (Note) shape type
f.VMLDrawing["xl/drawings/vmlDrawing1.vml"] = &vmlDrawing{
Shape: []xlsxShape{{Type: "#_x0000_t201", Val: "<x:ClientData ObjectType=\"Note\"></x:ClientData>"}},
}
formControls, err = f.GetFormControls("Sheet1")
assert.NoError(t, err)
assert.Len(t, formControls, 0)
assert.NoError(t, f.Close())
}
func TestExtractFormControl(t *testing.T) {
// Test extract form control with unsupported charset
_, err := extractFormControl(string(MacintoshCyrillicCharset))
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
}
func TestAddHeaderFooterImage(t *testing.T) {
f, sheet, wb := NewFile(), "Sheet1", filepath.Join("test", "TestAddHeaderFooterImage.xlsx")
headerFooterOptions := HeaderFooterOptions{
DifferentFirst: true,
OddHeader: "&L&GExcelize&C&G&R&G",
OddFooter: "&L&GExcelize&C&G&R&G",
FirstHeader: "&L&GExcelize&C&G&R&G",
FirstFooter: "&L&GExcelize&C&G&R&G",
}
assert.NoError(t, f.SetHeaderFooter(sheet, &headerFooterOptions))
assert.NoError(t, f.SetSheetView(sheet, -1, &ViewOptions{View: stringPtr("pageLayout")}))
images := map[string][]byte{
".wmf": nil, ".tif": nil, ".png": nil,
".jpg": nil, ".gif": nil, ".emz": nil, ".emf": nil,
}
for ext := range images {
img, err := os.ReadFile(filepath.Join("test", "images", "excel"+ext))
assert.NoError(t, err)
images[ext] = img
}
for _, opt := range []struct {
position HeaderFooterImagePositionType
file []byte
isFooter bool
firstPage bool
ext string
}{
{position: HeaderFooterImagePositionLeft, file: images[".tif"], firstPage: true, ext: ".tif"},
{position: HeaderFooterImagePositionCenter, file: images[".gif"], firstPage: true, ext: ".gif"},
{position: HeaderFooterImagePositionRight, file: images[".png"], firstPage: true, ext: ".png"},
{position: HeaderFooterImagePositionLeft, file: images[".emf"], isFooter: true, firstPage: true, ext: ".emf"},
{position: HeaderFooterImagePositionCenter, file: images[".wmf"], isFooter: true, firstPage: true, ext: ".wmf"},
{position: HeaderFooterImagePositionRight, file: images[".emz"], isFooter: true, firstPage: true, ext: ".emz"},
{position: HeaderFooterImagePositionLeft, file: images[".png"], ext: ".png"},
{position: HeaderFooterImagePositionCenter, file: images[".png"], ext: ".png"},
{position: HeaderFooterImagePositionRight, file: images[".png"], ext: ".png"},
{position: HeaderFooterImagePositionLeft, file: images[".tif"], isFooter: true, ext: ".tif"},
{position: HeaderFooterImagePositionCenter, file: images[".tif"], isFooter: true, ext: ".tif"},
{position: HeaderFooterImagePositionRight, file: images[".tif"], isFooter: true, ext: ".tif"},
} {
assert.NoError(t, f.AddHeaderFooterImage(sheet, &HeaderFooterImageOptions{
Position: opt.position,
File: opt.file,
IsFooter: opt.isFooter,
FirstPage: opt.firstPage,
Extension: opt.ext,
Width: "50pt",
Height: "32pt",
}))
}
assert.NoError(t, f.SetCellValue(sheet, "A1", "Example"))
// Test add header footer image with not exist sheet
assert.EqualError(t, f.AddHeaderFooterImage("SheetN", nil), "sheet SheetN does not exist")
// Test add header footer image with unsupported file type
assert.Equal(t, f.AddHeaderFooterImage(sheet, &HeaderFooterImageOptions{
Extension: "jpg",
}), ErrImgExt)
assert.NoError(t, f.SaveAs(wb))
assert.NoError(t, f.Close())
// Test change already exist header image with the different image
f, err := OpenFile(wb)
assert.NoError(t, err)
assert.NoError(t, f.AddHeaderFooterImage(sheet, &HeaderFooterImageOptions{
File: images[".jpg"],
FirstPage: true,
Extension: ".jpg",
Width: "50pt",
Height: "32pt",
}))
assert.NoError(t, f.Save())
assert.NoError(t, f.Close())
// Test add header image with unsupported charset VML drawing
f, err = OpenFile(wb)
assert.NoError(t, err)
f.Pkg.Store("xl/drawings/vmlDrawing1.vml", MacintoshCyrillicCharset)
assert.EqualError(t, f.AddHeaderFooterImage(sheet, &HeaderFooterImageOptions{
File: images[".jpg"],
Extension: ".jpg",
Width: "50pt",
Height: "32pt",
}), "XML syntax error on line 1: invalid UTF-8")
assert.NoError(t, f.Close())
// Test set legacy drawing header/footer with unsupported charset content types
f = NewFile()
f.ContentTypes = nil
f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
assert.EqualError(t, f.AddHeaderFooterImage(sheet, &HeaderFooterImageOptions{
Extension: ".png",
File: images[".png"],
Width: "50pt",
Height: "32pt",
}), "XML syntax error on line 1: invalid UTF-8")
assert.NoError(t, f.Close())
}

View File

@ -1,4 +1,4 @@
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in // this source code is governed by a BSD-style license that can be found in
// the LICENSE file. // the LICENSE file.
// //
@ -7,7 +7,7 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming // Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of // API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.16 or later. // data. This library needs Go version 1.18 or later.
package excelize package excelize
@ -145,8 +145,8 @@ func (f *File) setWorkbook(name string, sheetID, rid int) {
// the spreadsheet. // the spreadsheet.
func (f *File) getWorkbookPath() (path string) { func (f *File) getWorkbookPath() (path string) {
if rels, _ := f.relsReader("_rels/.rels"); rels != nil { if rels, _ := f.relsReader("_rels/.rels"); rels != nil {
rels.Lock() rels.mu.Lock()
defer rels.Unlock() defer rels.mu.Unlock()
for _, rel := range rels.Relationships { for _, rel := range rels.Relationships {
if rel.Type == SourceRelationshipOfficeDocument { if rel.Type == SourceRelationshipOfficeDocument {
path = strings.TrimPrefix(rel.Target, "/") path = strings.TrimPrefix(rel.Target, "/")
@ -170,6 +170,26 @@ func (f *File) getWorkbookRelsPath() (path string) {
return return
} }
// deleteWorkbookRels provides a function to delete relationships in
// xl/_rels/workbook.xml.rels by given type and target.
func (f *File) deleteWorkbookRels(relType, relTarget string) (string, error) {
var rID string
rels, err := f.relsReader(f.getWorkbookRelsPath())
if err != nil {
return rID, err
}
if rels == nil {
rels = &xlsxRelationships{}
}
for k, v := range rels.Relationships {
if v.Type == relType && v.Target == relTarget {
rID = v.ID
rels.Relationships = append(rels.Relationships[:k], rels.Relationships[k+1:]...)
}
}
return rID, err
}
// workbookReader provides a function to get the pointer to the workbook.xml // workbookReader provides a function to get the pointer to the workbook.xml
// structure after deserialization. // structure after deserialization.
func (f *File) workbookReader() (*xlsxWorkbook, error) { func (f *File) workbookReader() (*xlsxWorkbook, error) {
@ -177,9 +197,13 @@ func (f *File) workbookReader() (*xlsxWorkbook, error) {
if f.WorkBook == nil { if f.WorkBook == nil {
wbPath := f.getWorkbookPath() wbPath := f.getWorkbookPath()
f.WorkBook = new(xlsxWorkbook) f.WorkBook = new(xlsxWorkbook)
if _, ok := f.xmlAttr[wbPath]; !ok { if attrs, ok := f.xmlAttr.Load(wbPath); !ok {
d := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(wbPath)))) d := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(wbPath))))
f.xmlAttr[wbPath] = append(f.xmlAttr[wbPath], getRootElement(d)...) if attrs == nil {
attrs = []xml.Attr{}
}
attrs = append(attrs.([]xml.Attr), getRootElement(d)...)
f.xmlAttr.Store(wbPath, attrs)
f.addNameSpaces(wbPath, SourceRelationship) f.addNameSpaces(wbPath, SourceRelationship)
} }
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(wbPath)))). if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(wbPath)))).
@ -205,3 +229,150 @@ func (f *File) workBookWriter() {
f.saveFileList(f.getWorkbookPath(), replaceRelationshipsBytes(f.replaceNameSpaceBytes(f.getWorkbookPath(), output))) f.saveFileList(f.getWorkbookPath(), replaceRelationshipsBytes(f.replaceNameSpaceBytes(f.getWorkbookPath(), output)))
} }
} }
// setContentTypePartRelsExtensions provides a function to set the content type
// for relationship parts and the Main Document part.
func (f *File) setContentTypePartRelsExtensions() error {
var rels bool
content, err := f.contentTypesReader()
if err != nil {
return err
}
for _, v := range content.Defaults {
if v.Extension == "rels" {
rels = true
}
}
if !rels {
content.Defaults = append(content.Defaults, xlsxDefault{
Extension: "rels",
ContentType: ContentTypeRelationships,
})
}
return err
}
// setContentTypePartImageExtensions provides a function to set the content type
// for relationship parts and the Main Document part.
func (f *File) setContentTypePartImageExtensions() error {
imageTypes := map[string]string{
"bmp": "image/", "jpeg": "image/", "png": "image/", "gif": "image/",
"svg": "image/", "tiff": "image/", "emf": "image/x-", "wmf": "image/x-",
"emz": "image/x-", "wmz": "image/x-",
}
content, err := f.contentTypesReader()
if err != nil {
return err
}
content.mu.Lock()
defer content.mu.Unlock()
for _, file := range content.Defaults {
delete(imageTypes, file.Extension)
}
for extension, prefix := range imageTypes {
content.Defaults = append(content.Defaults, xlsxDefault{
Extension: extension,
ContentType: prefix + extension,
})
}
return err
}
// setContentTypePartVMLExtensions provides a function to set the content type
// for relationship parts and the Main Document part.
func (f *File) setContentTypePartVMLExtensions() error {
var vml bool
content, err := f.contentTypesReader()
if err != nil {
return err
}
content.mu.Lock()
defer content.mu.Unlock()
for _, v := range content.Defaults {
if v.Extension == "vml" {
vml = true
}
}
if !vml {
content.Defaults = append(content.Defaults, xlsxDefault{
Extension: "vml",
ContentType: ContentTypeVML,
})
}
return err
}
// addContentTypePart provides a function to add content type part relationships
// in the file [Content_Types].xml by given index and content type.
func (f *File) addContentTypePart(index int, contentType string) error {
setContentType := map[string]func() error{
"comments": f.setContentTypePartVMLExtensions,
"drawings": f.setContentTypePartImageExtensions,
}
partNames := map[string]string{
"chart": "/xl/charts/chart" + strconv.Itoa(index) + ".xml",
"chartsheet": "/xl/chartsheets/sheet" + strconv.Itoa(index) + ".xml",
"comments": "/xl/comments" + strconv.Itoa(index) + ".xml",
"drawings": "/xl/drawings/drawing" + strconv.Itoa(index) + ".xml",
"table": "/xl/tables/table" + strconv.Itoa(index) + ".xml",
"pivotTable": "/xl/pivotTables/pivotTable" + strconv.Itoa(index) + ".xml",
"pivotCache": "/xl/pivotCache/pivotCacheDefinition" + strconv.Itoa(index) + ".xml",
"sharedStrings": "/xl/sharedStrings.xml",
"slicer": "/xl/slicers/slicer" + strconv.Itoa(index) + ".xml",
"slicerCache": "/xl/slicerCaches/slicerCache" + strconv.Itoa(index) + ".xml",
}
contentTypes := map[string]string{
"chart": ContentTypeDrawingML,
"chartsheet": ContentTypeSpreadSheetMLChartsheet,
"comments": ContentTypeSpreadSheetMLComments,
"drawings": ContentTypeDrawing,
"table": ContentTypeSpreadSheetMLTable,
"pivotTable": ContentTypeSpreadSheetMLPivotTable,
"pivotCache": ContentTypeSpreadSheetMLPivotCacheDefinition,
"sharedStrings": ContentTypeSpreadSheetMLSharedStrings,
"slicer": ContentTypeSlicer,
"slicerCache": ContentTypeSlicerCache,
}
s, ok := setContentType[contentType]
if ok {
if err := s(); err != nil {
return err
}
}
content, err := f.contentTypesReader()
if err != nil {
return err
}
content.mu.Lock()
defer content.mu.Unlock()
for _, v := range content.Overrides {
if v.PartName == partNames[contentType] {
return err
}
}
content.Overrides = append(content.Overrides, xlsxOverride{
PartName: partNames[contentType],
ContentType: contentTypes[contentType],
})
return f.setContentTypePartRelsExtensions()
}
// removeContentTypesPart provides a function to remove relationships by given
// content type and part name in the file [Content_Types].xml.
func (f *File) removeContentTypesPart(contentType, partName string) error {
if !strings.HasPrefix(partName, "/") {
partName = "/xl/" + partName
}
content, err := f.contentTypesReader()
if err != nil {
return err
}
content.mu.Lock()
defer content.mu.Unlock()
for k, v := range content.Overrides {
if v.PartName == partName && v.ContentType == contentType {
content.Overrides = append(content.Overrides[:k], content.Overrides[k+1:]...)
}
}
return err
}

View File

@ -31,3 +31,13 @@ func TestWorkbookProps(t *testing.T) {
_, err = f.GetWorkbookProps() _, err = f.GetWorkbookProps()
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8") assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
} }
func TestDeleteWorkbookRels(t *testing.T) {
f := NewFile()
// Test delete pivot table without worksheet relationships
f.Relationships.Delete("xl/_rels/workbook.xml.rels")
f.Pkg.Delete("xl/_rels/workbook.xml.rels")
rID, err := f.deleteWorkbookRels("", "")
assert.Empty(t, rID)
assert.NoError(t, err)
}

View File

@ -1,4 +1,4 @@
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in // this source code is governed by a BSD-style license that can be found in
// the LICENSE file. // the LICENSE file.
// //
@ -7,7 +7,7 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming // Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of // API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.16 or later. // data. This library needs Go version 1.18 or later.
package excelize package excelize

View File

@ -1,4 +1,4 @@
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in // this source code is governed by a BSD-style license that can be found in
// the LICENSE file. // the LICENSE file.
// //
@ -7,13 +7,14 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming // Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of // API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.16 or later. // data. This library needs Go version 1.18 or later.
package excelize package excelize
import "encoding/xml" import "encoding/xml"
// xlsxCalcChain directly maps the calcChain element. This element represents the root of the calculation chain. // xlsxCalcChain directly maps the calcChain element. This element represents
// the root of the calculation chain.
type xlsxCalcChain struct { type xlsxCalcChain struct {
XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main calcChain"` XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main calcChain"`
C []xlsxCalcChainC `xml:"c"` C []xlsxCalcChainC `xml:"c"`
@ -76,9 +77,48 @@ type xlsxCalcChain struct {
// | boolean datatype. // | boolean datatype.
type xlsxCalcChainC struct { type xlsxCalcChainC struct {
R string `xml:"r,attr"` R string `xml:"r,attr"`
I int `xml:"i,attr"` I int `xml:"i,attr,omitempty"`
L bool `xml:"l,attr,omitempty"` L bool `xml:"l,attr,omitempty"`
S bool `xml:"s,attr,omitempty"` S bool `xml:"s,attr,omitempty"`
T bool `xml:"t,attr,omitempty"` T bool `xml:"t,attr,omitempty"`
A bool `xml:"a,attr,omitempty"` A bool `xml:"a,attr,omitempty"`
} }
// xlsxVolTypes maps the volatileDependencies part provides a cache of data that
// supports Real Time Data (RTD) and CUBE functions in the workbook.
type xlsxVolTypes struct {
XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main volTypes"`
VolType []xlsxVolType `xml:"volType"`
ExtLst *xlsxExtLst `xml:"extLst"`
}
// xlsxVolType represents dependency information for a specific type of external
// data server.
type xlsxVolType struct {
Type string `xml:"type,attr"`
Main []xlsxVolMain `xml:"main"`
}
// xlsxVolMain represents dependency information for all topics within a
// volatile dependency type that share the same first string or function
// argument.
type xlsxVolMain struct {
First string `xml:"first,attr"`
Tp []xlsxVolTopic `xml:"tp"`
}
// xlsxVolTopic represents dependency information for all topics within a
// volatile dependency type that share the same first string or argument.
type xlsxVolTopic struct {
T string `xml:"t,attr,omitempty"`
V string `xml:"v"`
Stp []string `xml:"stp"`
Tr []xlsxVolTopicRef `xml:"tr"`
}
// xlsxVolTopicRef represents the reference to a cell that depends on this
// topic. Each topic can have one or more cells dependencies.
type xlsxVolTopicRef struct {
R string `xml:"r,attr"`
S int `xml:"s,attr"`
}

View File

@ -1,4 +1,4 @@
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in // this source code is governed by a BSD-style license that can be found in
// the LICENSE file. // the LICENSE file.
// //
@ -7,7 +7,7 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming // Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of // API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.16 or later. // data. This library needs Go version 1.18 or later.
package excelize package excelize
@ -74,7 +74,7 @@ type cTx struct {
type cRich struct { type cRich struct {
BodyPr aBodyPr `xml:"a:bodyPr,omitempty"` BodyPr aBodyPr `xml:"a:bodyPr,omitempty"`
LstStyle string `xml:"a:lstStyle,omitempty"` LstStyle string `xml:"a:lstStyle,omitempty"`
P aP `xml:"a:p"` P []aP `xml:"a:p"`
} }
// aBodyPr (Body Properties) directly maps the a:bodyPr element. This element // aBodyPr (Body Properties) directly maps the a:bodyPr element. This element
@ -248,13 +248,13 @@ type aContourClr struct {
// shapes and text. The line allows for the specifying of many different types // shapes and text. The line allows for the specifying of many different types
// of outlines including even line dashes and bevels. // of outlines including even line dashes and bevels.
type aLn struct { type aLn struct {
Algn string `xml:"algn,attr,omitempty"` Algn string `xml:"algn,attr,omitempty"`
Cap string `xml:"cap,attr,omitempty"` Cap string `xml:"cap,attr,omitempty"`
Cmpd string `xml:"cmpd,attr,omitempty"` Cmpd string `xml:"cmpd,attr,omitempty"`
W int `xml:"w,attr,omitempty"` W int `xml:"w,attr,omitempty"`
NoFill string `xml:"a:noFill,omitempty"` NoFill *attrValString `xml:"a:noFill"`
Round string `xml:"a:round,omitempty"` Round string `xml:"a:round,omitempty"`
SolidFill *aSolidFill `xml:"a:solidFill"` SolidFill *aSolidFill `xml:"a:solidFill"`
} }
// cTxPr (Text Properties) directly maps the txPr element. This element // cTxPr (Text Properties) directly maps the txPr element. This element
@ -300,26 +300,26 @@ type cView3D struct {
// cPlotArea directly maps the plotArea element. This element specifies the // cPlotArea directly maps the plotArea element. This element specifies the
// plot area of the chart. // plot area of the chart.
type cPlotArea struct { type cPlotArea struct {
Layout *string `xml:"layout"` Layout *string `xml:"layout"`
AreaChart *cCharts `xml:"areaChart"` AreaChart []*cCharts `xml:"areaChart"`
Area3DChart *cCharts `xml:"area3DChart"` Area3DChart []*cCharts `xml:"area3DChart"`
BarChart *cCharts `xml:"barChart"` BarChart []*cCharts `xml:"barChart"`
Bar3DChart *cCharts `xml:"bar3DChart"` Bar3DChart []*cCharts `xml:"bar3DChart"`
BubbleChart *cCharts `xml:"bubbleChart"` BubbleChart []*cCharts `xml:"bubbleChart"`
DoughnutChart *cCharts `xml:"doughnutChart"` DoughnutChart []*cCharts `xml:"doughnutChart"`
LineChart *cCharts `xml:"lineChart"` LineChart []*cCharts `xml:"lineChart"`
Line3DChart *cCharts `xml:"line3DChart"` Line3DChart []*cCharts `xml:"line3DChart"`
PieChart *cCharts `xml:"pieChart"` PieChart []*cCharts `xml:"pieChart"`
Pie3DChart *cCharts `xml:"pie3DChart"` Pie3DChart []*cCharts `xml:"pie3DChart"`
OfPieChart *cCharts `xml:"ofPieChart"` OfPieChart []*cCharts `xml:"ofPieChart"`
RadarChart *cCharts `xml:"radarChart"` RadarChart []*cCharts `xml:"radarChart"`
ScatterChart *cCharts `xml:"scatterChart"` ScatterChart []*cCharts `xml:"scatterChart"`
Surface3DChart *cCharts `xml:"surface3DChart"` Surface3DChart []*cCharts `xml:"surface3DChart"`
SurfaceChart *cCharts `xml:"surfaceChart"` SurfaceChart []*cCharts `xml:"surfaceChart"`
CatAx []*cAxs `xml:"catAx"` CatAx []*cAxs `xml:"catAx"`
ValAx []*cAxs `xml:"valAx"` ValAx []*cAxs `xml:"valAx"`
SerAx []*cAxs `xml:"serAx"` SerAx []*cAxs `xml:"serAx"`
SpPr *cSpPr `xml:"spPr"` SpPr *cSpPr `xml:"spPr"`
} }
// cCharts specifies the common element of the chart. // cCharts specifies the common element of the chart.
@ -351,6 +351,7 @@ type cAxs struct {
AxPos *attrValString `xml:"axPos"` AxPos *attrValString `xml:"axPos"`
MajorGridlines *cChartLines `xml:"majorGridlines"` MajorGridlines *cChartLines `xml:"majorGridlines"`
MinorGridlines *cChartLines `xml:"minorGridlines"` MinorGridlines *cChartLines `xml:"minorGridlines"`
Title *cTitle `xml:"title"`
NumFmt *cNumFmt `xml:"numFmt"` NumFmt *cNumFmt `xml:"numFmt"`
MajorTickMark *attrValString `xml:"majorTickMark"` MajorTickMark *attrValString `xml:"majorTickMark"`
MinorTickMark *attrValString `xml:"minorTickMark"` MinorTickMark *attrValString `xml:"minorTickMark"`
@ -481,14 +482,15 @@ type cNumCache struct {
// entire series or the entire chart. It contains child elements that specify // entire series or the entire chart. It contains child elements that specify
// the specific formatting and positioning settings. // the specific formatting and positioning settings.
type cDLbls struct { type cDLbls struct {
NumFmt *cNumFmt `xml:"numFmt"` NumFmt *cNumFmt `xml:"numFmt"`
ShowLegendKey *attrValBool `xml:"showLegendKey"` DLblPos *attrValString `xml:"dLblPos"`
ShowVal *attrValBool `xml:"showVal"` ShowLegendKey *attrValBool `xml:"showLegendKey"`
ShowCatName *attrValBool `xml:"showCatName"` ShowVal *attrValBool `xml:"showVal"`
ShowSerName *attrValBool `xml:"showSerName"` ShowCatName *attrValBool `xml:"showCatName"`
ShowPercent *attrValBool `xml:"showPercent"` ShowSerName *attrValBool `xml:"showSerName"`
ShowBubbleSize *attrValBool `xml:"showBubbleSize"` ShowPercent *attrValBool `xml:"showPercent"`
ShowLeaderLines *attrValBool `xml:"showLeaderLines"` ShowBubbleSize *attrValBool `xml:"showBubbleSize"`
ShowLeaderLines *attrValBool `xml:"showLeaderLines"`
} }
// cLegend (Legend) directly maps the legend element. This element specifies // cLegend (Legend) directly maps the legend element. This element specifies
@ -528,17 +530,22 @@ type ChartNumFmt struct {
// ChartAxis directly maps the format settings of the chart axis. // ChartAxis directly maps the format settings of the chart axis.
type ChartAxis struct { type ChartAxis struct {
None bool None bool
MajorGridLines bool MajorGridLines bool
MinorGridLines bool MinorGridLines bool
MajorUnit float64 MajorUnit float64
TickLabelSkip int TickLabelPosition ChartTickLabelPositionType
ReverseOrder bool TickLabelSkip int
Maximum *float64 ReverseOrder bool
Minimum *float64 Secondary bool
Font Font Maximum *float64
LogBase float64 Minimum *float64
NumFmt ChartNumFmt Alignment Alignment
Font Font
LogBase float64
NumFmt ChartNumFmt
Title []RichTextRun
axID int
} }
// ChartDimension directly maps the dimension of the chart. // ChartDimension directly maps the dimension of the chart.
@ -556,6 +563,7 @@ type ChartPlotArea struct {
ShowPercent bool ShowPercent bool
ShowSerName bool ShowSerName bool
ShowVal bool ShowVal bool
Fill Fill
NumFmt ChartNumFmt NumFmt ChartNumFmt
} }
@ -566,12 +574,15 @@ type Chart struct {
Format GraphicOptions Format GraphicOptions
Dimension ChartDimension Dimension ChartDimension
Legend ChartLegend Legend ChartLegend
Title ChartTitle Title []RichTextRun
VaryColors *bool VaryColors *bool
XAxis ChartAxis XAxis ChartAxis
YAxis ChartAxis YAxis ChartAxis
PlotArea ChartPlotArea PlotArea ChartPlotArea
Fill Fill
Border ChartLine
ShowBlanksAs string ShowBlanksAs string
BubbleSize int
HoleSize int HoleSize int
order int order int
} }
@ -584,28 +595,26 @@ type ChartLegend struct {
// ChartMarker directly maps the format settings of the chart marker. // ChartMarker directly maps the format settings of the chart marker.
type ChartMarker struct { type ChartMarker struct {
Fill Fill
Symbol string Symbol string
Size int Size int
} }
// ChartLine directly maps the format settings of the chart line. // ChartLine directly maps the format settings of the chart line.
type ChartLine struct { type ChartLine struct {
Type ChartLineType
Smooth bool Smooth bool
Width float64 Width float64
} }
// ChartSeries directly maps the format settings of the chart series. // ChartSeries directly maps the format settings of the chart series.
type ChartSeries struct { type ChartSeries struct {
Name string Name string
Categories string Categories string
Sizes string Values string
Values string Sizes string
Fill Fill Fill Fill
Line ChartLine Line ChartLine
Marker ChartMarker Marker ChartMarker
} DataLabelPosition ChartDataLabelPositionType
// ChartTitle directly maps the format settings of the chart title.
type ChartTitle struct {
Name string
} }

View File

@ -1,4 +1,4 @@
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in // this source code is governed by a BSD-style license that can be found in
// the LICENSE file. // the LICENSE file.
// //
@ -9,7 +9,7 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming // Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of // API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.16 or later. // data. This library needs Go version 1.18 or later.
package excelize package excelize
@ -35,10 +35,10 @@ type xlsxChartsheet struct {
// xlsxChartsheetPr specifies chart sheet properties. // xlsxChartsheetPr specifies chart sheet properties.
type xlsxChartsheetPr struct { type xlsxChartsheetPr struct {
XMLName xml.Name `xml:"sheetPr"` XMLName xml.Name `xml:"sheetPr"`
PublishedAttr bool `xml:"published,attr,omitempty"` PublishedAttr bool `xml:"published,attr,omitempty"`
CodeNameAttr string `xml:"codeName,attr,omitempty"` CodeNameAttr string `xml:"codeName,attr,omitempty"`
TabColor *xlsxTabColor `xml:"tabColor"` TabColor *xlsxColor `xml:"tabColor"`
} }
// xlsxChartsheetViews specifies chart sheet views. // xlsxChartsheetViews specifies chart sheet views.

View File

@ -1,4 +1,4 @@
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in // this source code is governed by a BSD-style license that can be found in
// the LICENSE file. // the LICENSE file.
// //
@ -7,7 +7,7 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming // Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of // API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.16 or later. // data. This library needs Go version 1.18 or later.
package excelize package excelize
@ -74,9 +74,11 @@ type xlsxPhoneticRun struct {
// Comment directly maps the comment information. // Comment directly maps the comment information.
type Comment struct { type Comment struct {
Author string Author string
AuthorID int AuthorID int
Cell string Cell string
Text string Text string
Runs []RichTextRun Width uint
Height uint
Paragraph []RichTextRun
} }

View File

@ -1,4 +1,4 @@
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in // this source code is governed by a BSD-style license that can be found in
// the LICENSE file. // the LICENSE file.
// //
@ -7,7 +7,7 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming // Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of // API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.16 or later. // data. This library needs Go version 1.18 or later.
package excelize package excelize
@ -20,7 +20,7 @@ import (
// parts, it takes a Multipurpose Internet Mail Extension (MIME) media type as a // parts, it takes a Multipurpose Internet Mail Extension (MIME) media type as a
// value. // value.
type xlsxTypes struct { type xlsxTypes struct {
sync.Mutex mu sync.Mutex
XMLName xml.Name `xml:"http://schemas.openxmlformats.org/package/2006/content-types Types"` XMLName xml.Name `xml:"http://schemas.openxmlformats.org/package/2006/content-types Types"`
Defaults []xlsxDefault `xml:"Default"` Defaults []xlsxDefault `xml:"Default"`
Overrides []xlsxOverride `xml:"Override"` Overrides []xlsxOverride `xml:"Override"`

View File

@ -1,4 +1,4 @@
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in // this source code is governed by a BSD-style license that can be found in
// the LICENSE file. // the LICENSE file.
// //
@ -7,7 +7,7 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming // Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of // API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.16 or later. // data. This library needs Go version 1.18 or later.
package excelize package excelize

View File

@ -1,4 +1,4 @@
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in // this source code is governed by a BSD-style license that can be found in
// the LICENSE file. // the LICENSE file.
// //
@ -7,7 +7,7 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming // Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of // API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.16 or later. // data. This library needs Go version 1.18 or later.
package excelize package excelize
@ -18,27 +18,67 @@ import "encoding/xml"
// specifies a two cell anchor placeholder for a group, a shape, or a drawing // specifies a two cell anchor placeholder for a group, a shape, or a drawing
// element. It moves with cells and its extents are in EMU units. // element. It moves with cells and its extents are in EMU units.
type decodeCellAnchor struct { type decodeCellAnchor struct {
EditAs string `xml:"editAs,attr,omitempty"` EditAs string `xml:"editAs,attr,omitempty"`
From *decodeFrom `xml:"from"` From *decodeFrom `xml:"from"`
To *decodeTo `xml:"to"` To *decodeTo `xml:"to"`
Sp *decodeSp `xml:"sp"` Sp *decodeSp `xml:"sp"`
ClientData *decodeClientData `xml:"clientData"` Pic *decodePic `xml:"pic"`
Content string `xml:",innerxml"` ClientData *decodeClientData `xml:"clientData"`
AlternateContent []*xlsxAlternateContent `xml:"AlternateContent"`
Content string `xml:",innerxml"`
} }
// xdrSp (Shape) directly maps the sp element. This element specifies the // decodeCellAnchorPos defines the structure used to deserialize the cell anchor
// existence of a single shape. A shape can either be a preset or a custom // for adjust drawing object on inserting/deleting column/rows.
// geometry, defined using the SpreadsheetDrawingML framework. In addition to type decodeCellAnchorPos struct {
// a geometry each shape can have both visual and non-visual properties EditAs string `xml:"editAs,attr,omitempty"`
// attached. Text and corresponding styling information can also be attached From *xlsxFrom `xml:"from"`
// to a shape. This shape is specified along with all other shapes within To *xlsxTo `xml:"to"`
// either the shape tree or group shape elements. Pos *xlsxInnerXML `xml:"pos"`
Ext *xlsxInnerXML `xml:"ext"`
Sp *xlsxSp `xml:"sp"`
GrpSp *xlsxInnerXML `xml:"grpSp"`
GraphicFrame *xlsxInnerXML `xml:"graphicFrame"`
CxnSp *xlsxInnerXML `xml:"cxnSp"`
Pic *xlsxInnerXML `xml:"pic"`
ContentPart *xlsxInnerXML `xml:"contentPart"`
AlternateContent []*xlsxAlternateContent `xml:"AlternateContent"`
ClientData *xlsxInnerXML `xml:"clientData"`
}
// decodeChoice defines the structure used to deserialize the mc:Choice element.
type decodeChoice struct {
XMLName xml.Name `xml:"Choice"`
XMLNSA14 string `xml:"a14,attr"`
XMLNSSle15 string `xml:"sle15,attr"`
Requires string `xml:"Requires,attr"`
GraphicFrame decodeGraphicFrame `xml:"graphicFrame"`
}
// decodeGraphicFrame defines the structure used to deserialize the
// xdr:graphicFrame element.
type decodeGraphicFrame struct {
Macro string `xml:"macro,attr"`
NvGraphicFramePr decodeNvGraphicFramePr `xml:"nvGraphicFramePr"`
}
// decodeNvGraphicFramePr defines the structure used to deserialize the
// xdr:nvGraphicFramePr element.
type decodeNvGraphicFramePr struct {
CNvPr decodeCNvPr `xml:"cNvPr"`
}
// decodeSp defines the structure used to deserialize the sp element.
type decodeSp struct { type decodeSp struct {
NvSpPr *decodeNvSpPr `xml:"nvSpPr"` Macro string `xml:"macro,attr,omitempty"`
SpPr *decodeSpPr `xml:"spPr"` TextLink string `xml:"textlink,attr,omitempty"`
FLocksText bool `xml:"fLocksText,attr,omitempty"`
FPublished *bool `xml:"fPublished,attr"`
NvSpPr *decodeNvSpPr `xml:"nvSpPr"`
SpPr *decodeSpPr `xml:"spPr"`
} }
// decodeSp (Non-Visual Properties for a Shape) directly maps the nvSpPr // decodeNvSpPr (Non-Visual Properties for a Shape) directly maps the nvSpPr
// element. This element specifies all non-visual properties for a shape. This // element. This element specifies all non-visual properties for a shape. This
// element is a container for the non-visual identification properties, shape // element is a container for the non-visual identification properties, shape
// properties and application properties that are to be associated with a // properties and application properties that are to be associated with a
@ -46,7 +86,7 @@ type decodeSp struct {
// appearance of the shape to be stored. // appearance of the shape to be stored.
type decodeNvSpPr struct { type decodeNvSpPr struct {
CNvPr *decodeCNvPr `xml:"cNvPr"` CNvPr *decodeCNvPr `xml:"cNvPr"`
ExtLst *decodeExt `xml:"extLst"` ExtLst *decodeAExt `xml:"extLst"`
CNvSpPr *decodeCNvSpPr `xml:"cNvSpPr"` CNvSpPr *decodeCNvSpPr `xml:"cNvSpPr"`
} }
@ -63,7 +103,7 @@ type decodeCNvSpPr struct {
// changed after serialization and deserialization, two different structures // changed after serialization and deserialization, two different structures
// are defined. decodeWsDr just for deserialization. // are defined. decodeWsDr just for deserialization.
type decodeWsDr struct { type decodeWsDr struct {
XMLName xml.Name `xml:"http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing wsDr,omitempty"` XMLName xml.Name `xml:"http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing wsDr"`
A string `xml:"xmlns a,attr"` A string `xml:"xmlns a,attr"`
Xdr string `xml:"xmlns xdr,attr"` Xdr string `xml:"xmlns xdr,attr"`
R string `xml:"xmlns r,attr"` R string `xml:"xmlns r,attr"`
@ -72,26 +112,16 @@ type decodeWsDr struct {
TwoCellAnchor []*decodeCellAnchor `xml:"twoCellAnchor,omitempty"` TwoCellAnchor []*decodeCellAnchor `xml:"twoCellAnchor,omitempty"`
} }
// decodeTwoCellAnchor directly maps the oneCellAnchor (One Cell Anchor Shape
// Size) and twoCellAnchor (Two Cell Anchor Shape Size). This element
// specifies a two cell anchor placeholder for a group, a shape, or a drawing
// element. It moves with cells and its extents are in EMU units.
type decodeTwoCellAnchor struct {
From *decodeFrom `xml:"from"`
To *decodeTo `xml:"to"`
Pic *decodePic `xml:"pic"`
ClientData *decodeClientData `xml:"clientData"`
}
// decodeCNvPr directly maps the cNvPr (Non-Visual Drawing Properties). This // decodeCNvPr directly maps the cNvPr (Non-Visual Drawing Properties). This
// element specifies non-visual canvas properties. This allows for additional // element specifies non-visual canvas properties. This allows for additional
// information that does not affect the appearance of the picture to be // information that does not affect the appearance of the picture to be
// stored. // stored.
type decodeCNvPr struct { type decodeCNvPr struct {
ID int `xml:"id,attr"` XMLName xml.Name `xml:"cNvPr"`
Name string `xml:"name,attr"` ID int `xml:"id,attr"`
Descr string `xml:"descr,attr"` Name string `xml:"name,attr"`
Title string `xml:"title,attr,omitempty"` Descr string `xml:"descr,attr"`
Title string `xml:"title,attr,omitempty"`
} }
// decodePicLocks directly maps the picLocks (Picture Locks). This element // decodePicLocks directly maps the picLocks (Picture Locks). This element
@ -134,8 +164,8 @@ type decodeOff struct {
Y int `xml:"y,attr"` Y int `xml:"y,attr"`
} }
// decodeExt directly maps the ext element. // decodeAExt directly maps the a:ext element.
type decodeExt struct { type decodeAExt struct {
Cx int `xml:"cx,attr"` Cx int `xml:"cx,attr"`
Cy int `xml:"cy,attr"` Cy int `xml:"cy,attr"`
} }
@ -153,8 +183,8 @@ type decodePrstGeom struct {
// frame. This transformation is applied to the graphic frame just as it would // frame. This transformation is applied to the graphic frame just as it would
// be for a shape or group shape. // be for a shape or group shape.
type decodeXfrm struct { type decodeXfrm struct {
Off decodeOff `xml:"off"` Off decodeOff `xml:"off"`
Ext decodeExt `xml:"ext"` Ext decodeAExt `xml:"ext"`
} }
// decodeCNvPicPr directly maps the cNvPicPr (Non-Visual Picture Drawing // decodeCNvPicPr directly maps the cNvPicPr (Non-Visual Picture Drawing
@ -199,7 +229,7 @@ type decodeSpPr struct {
// decodePic elements encompass the definition of pictures within the // decodePic elements encompass the definition of pictures within the
// DrawingML framework. While pictures are in many ways very similar to shapes // DrawingML framework. While pictures are in many ways very similar to shapes
// they have specific properties that are unique in order to optimize for // they have specific properties that are unique in order to optimize for
// picture- specific scenarios. // picture-specific scenarios.
type decodePic struct { type decodePic struct {
NvPicPr decodeNvPicPr `xml:"nvPicPr"` NvPicPr decodeNvPicPr `xml:"nvPicPr"`
BlipFill decodeBlipFill `xml:"blipFill"` BlipFill decodeBlipFill `xml:"blipFill"`
@ -232,3 +262,15 @@ type decodeClientData struct {
FLocksWithSheet bool `xml:"fLocksWithSheet,attr"` FLocksWithSheet bool `xml:"fLocksWithSheet,attr"`
FPrintsWithSheet bool `xml:"fPrintsWithSheet,attr"` FPrintsWithSheet bool `xml:"fPrintsWithSheet,attr"`
} }
// decodeCellImages directly maps the Kingsoft WPS Office embedded cell images.
type decodeCellImages struct {
XMLName xml.Name `xml:"http://www.wps.cn/officeDocument/2017/etCustomData cellImages"`
CellImage []decodeCellImage `xml:"cellImage"`
}
// decodeCellImage defines the structure used to deserialize the Kingsoft WPS
// Office embedded cell images.
type decodeCellImage struct {
Pic decodePic `xml:"pic"`
}

View File

@ -1,4 +1,4 @@
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in // this source code is governed by a BSD-style license that can be found in
// the LICENSE file. // the LICENSE file.
// //
@ -7,7 +7,7 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming // Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of // API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.16 or later. // data. This library needs Go version 1.18 or later.
package excelize package excelize
@ -16,208 +16,6 @@ import (
"sync" "sync"
) )
// Source relationship and namespace list, associated prefixes and schema in which it was
// introduced.
var (
NameSpaceDocumentPropertiesVariantTypes = xml.Attr{Name: xml.Name{Local: "vt", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"}
NameSpaceDrawing2016SVG = xml.Attr{Name: xml.Name{Local: "asvg", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/drawing/2016/SVG/main"}
NameSpaceDrawingML = xml.Attr{Name: xml.Name{Local: "a", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/drawingml/2006/main"}
NameSpaceDrawingMLChart = xml.Attr{Name: xml.Name{Local: "c", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/drawingml/2006/chart"}
NameSpaceDrawingMLSpreadSheet = xml.Attr{Name: xml.Name{Local: "xdr", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing"}
NameSpaceMacExcel2008Main = xml.Attr{Name: xml.Name{Local: "mx", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/mac/excel/2008/main"}
NameSpaceSpreadSheet = xml.Attr{Name: xml.Name{Local: "xmlns"}, Value: "http://schemas.openxmlformats.org/spreadsheetml/2006/main"}
NameSpaceSpreadSheetExcel2006Main = xml.Attr{Name: xml.Name{Local: "xne", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/excel/2006/main"}
NameSpaceSpreadSheetX14 = xml.Attr{Name: xml.Name{Local: "x14", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main"}
NameSpaceSpreadSheetX15 = xml.Attr{Name: xml.Name{Local: "x15", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/spreadsheetml/2010/11/main"}
SourceRelationship = xml.Attr{Name: xml.Name{Local: "r", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/officeDocument/2006/relationships"}
SourceRelationshipChart20070802 = xml.Attr{Name: xml.Name{Local: "c14", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/drawing/2007/8/2/chart"}
SourceRelationshipChart2014 = xml.Attr{Name: xml.Name{Local: "c16", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/drawing/2014/chart"}
SourceRelationshipChart201506 = xml.Attr{Name: xml.Name{Local: "c16r2", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/drawing/2015/06/chart"}
SourceRelationshipCompatibility = xml.Attr{Name: xml.Name{Local: "mc", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/markup-compatibility/2006"}
)
// Source relationship and namespace.
const (
ContentTypeAddinMacro = "application/vnd.ms-excel.addin.macroEnabled.main+xml"
ContentTypeDrawing = "application/vnd.openxmlformats-officedocument.drawing+xml"
ContentTypeDrawingML = "application/vnd.openxmlformats-officedocument.drawingml.chart+xml"
ContentTypeMacro = "application/vnd.ms-excel.sheet.macroEnabled.main+xml"
ContentTypeSheetML = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"
ContentTypeSpreadSheetMLChartsheet = "application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml"
ContentTypeSpreadSheetMLComments = "application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml"
ContentTypeSpreadSheetMLPivotCacheDefinition = "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml"
ContentTypeSpreadSheetMLPivotTable = "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml"
ContentTypeSpreadSheetMLSharedStrings = "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"
ContentTypeSpreadSheetMLTable = "application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml"
ContentTypeSpreadSheetMLWorksheet = "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"
ContentTypeTemplate = "application/vnd.openxmlformats-officedocument.spreadsheetml.template.main+xml"
ContentTypeTemplateMacro = "application/vnd.ms-excel.template.macroEnabled.main+xml"
ContentTypeVBA = "application/vnd.ms-office.vbaProject"
ContentTypeVML = "application/vnd.openxmlformats-officedocument.vmlDrawing"
NameSpaceDrawingMLMain = "http://schemas.openxmlformats.org/drawingml/2006/main"
NameSpaceDublinCore = "http://purl.org/dc/elements/1.1/"
NameSpaceDublinCoreMetadataInitiative = "http://purl.org/dc/dcmitype/"
NameSpaceDublinCoreTerms = "http://purl.org/dc/terms/"
NameSpaceExtendedProperties = "http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"
NameSpaceXML = "http://www.w3.org/XML/1998/namespace"
NameSpaceXMLSchemaInstance = "http://www.w3.org/2001/XMLSchema-instance"
SourceRelationshipChart = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart"
SourceRelationshipChartsheet = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chartsheet"
SourceRelationshipComments = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments"
SourceRelationshipDialogsheet = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/dialogsheet"
SourceRelationshipDrawingML = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing"
SourceRelationshipDrawingVML = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing"
SourceRelationshipExtendProperties = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties"
SourceRelationshipHyperLink = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink"
SourceRelationshipImage = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image"
SourceRelationshipOfficeDocument = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument"
SourceRelationshipPivotCache = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition"
SourceRelationshipPivotTable = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable"
SourceRelationshipSharedStrings = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings"
SourceRelationshipTable = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/table"
SourceRelationshipVBAProject = "http://schemas.microsoft.com/office/2006/relationships/vbaProject"
SourceRelationshipWorkSheet = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"
StrictNameSpaceDocumentPropertiesVariantTypes = "http://purl.oclc.org/ooxml/officeDocument/docPropsVTypes"
StrictNameSpaceDrawingMLMain = "http://purl.oclc.org/ooxml/drawingml/main"
StrictNameSpaceExtendedProperties = "http://purl.oclc.org/ooxml/officeDocument/extendedProperties"
StrictNameSpaceSpreadSheet = "http://purl.oclc.org/ooxml/spreadsheetml/main"
StrictSourceRelationship = "http://purl.oclc.org/ooxml/officeDocument/relationships"
StrictSourceRelationshipChart = "http://purl.oclc.org/ooxml/officeDocument/relationships/chart"
StrictSourceRelationshipComments = "http://purl.oclc.org/ooxml/officeDocument/relationships/comments"
StrictSourceRelationshipExtendProperties = "http://purl.oclc.org/ooxml/officeDocument/relationships/extendedProperties"
StrictSourceRelationshipImage = "http://purl.oclc.org/ooxml/officeDocument/relationships/image"
StrictSourceRelationshipOfficeDocument = "http://purl.oclc.org/ooxml/officeDocument/relationships/officeDocument"
// ExtURIConditionalFormattings is the extLst child element
// ([ISO/IEC29500-1:2016] section 18.2.10) of the worksheet element
// ([ISO/IEC29500-1:2016] section 18.3.1.99) is extended by the addition of
// new child ext elements ([ISO/IEC29500-1:2016] section 18.2.7)
ExtURIConditionalFormattingRuleID = "{B025F937-C7B1-47D3-B67F-A62EFF666E3E}"
ExtURIConditionalFormattings = "{78C0D931-6437-407d-A8EE-F0AAD7539E65}"
ExtURIDataValidations = "{CCE6A557-97BC-4B89-ADB6-D9C93CAAB3DF}"
ExtURIDrawingBlip = "{28A0092B-C50C-407E-A947-70E740481C1C}"
ExtURIIgnoredErrors = "{01252117-D84E-4E92-8308-4BE1C098FCBB}"
ExtURIMacExcelMX = "{64002731-A6B0-56B0-2670-7721B7C09600}"
ExtURIProtectedRanges = "{FC87AEE6-9EDD-4A0A-B7FB-166176984837}"
ExtURISlicerCachesListX14 = "{BBE1A952-AA13-448e-AADC-164F8A28A991}"
ExtURISlicerListX14 = "{A8765BA9-456A-4DAB-B4F3-ACF838C121DE}"
ExtURISlicerListX15 = "{3A4CF648-6AED-40f4-86FF-DC5316D8AED3}"
ExtURISparklineGroups = "{05C60535-1F16-4fd2-B633-F4F36F0B64E0}"
ExtURISVG = "{96DAC541-7B7A-43D3-8B79-37D633B846F1}"
ExtURITimelineRefs = "{7E03D99C-DC04-49d9-9315-930204A7B6E9}"
ExtURIWebExtensions = "{F7C9EE02-42E1-4005-9D12-6889AFFD525C}"
)
// extensionURIPriority is the priority of URI in the extension lists.
var extensionURIPriority = []string{
ExtURIConditionalFormattings,
ExtURIDataValidations,
ExtURISparklineGroups,
ExtURISlicerListX14,
ExtURIProtectedRanges,
ExtURIIgnoredErrors,
ExtURIWebExtensions,
ExtURITimelineRefs,
}
// Excel specifications and limits
const (
MaxCellStyles = 65430
MaxColumns = 16384
MaxColumnWidth = 255
MaxFieldLength = 255
MaxFilePathLength = 207
MaxFontFamilyLength = 31
MaxFontSize = 409
MaxRowHeight = 409
MaxSheetNameLength = 31
MinColumns = 1
MinFontSize = 1
StreamChunkSize = 1 << 24
TotalCellChars = 32767
TotalRows = 1048576
TotalSheetHyperlinks = 65529
UnzipSizeLimit = 1000 << 24
// pivotTableVersion should be greater than 3. One or more of the
// PivotTables chosen are created in a version of Excel earlier than
// Excel 2007 or in compatibility mode. Slicer can only be used with
// PivotTables created in Excel 2007 or a newer version of Excel.
pivotTableVersion = 3
defaultPictureScale = 1.0
defaultChartDimensionWidth = 480
defaultChartDimensionHeight = 290
defaultChartLegendPosition = "bottom"
defaultChartShowBlanksAs = "gap"
defaultShapeSize = 160
defaultShapeLineWidth = 1
)
// ColorMappingType is the type of color transformation.
type ColorMappingType byte
// Color transformation types enumeration.
const (
ColorMappingTypeLight1 ColorMappingType = iota
ColorMappingTypeDark1
ColorMappingTypeLight2
ColorMappingTypeDark2
ColorMappingTypeAccent1
ColorMappingTypeAccent2
ColorMappingTypeAccent3
ColorMappingTypeAccent4
ColorMappingTypeAccent5
ColorMappingTypeAccent6
ColorMappingTypeHyperlink
ColorMappingTypeFollowedHyperlink
ColorMappingTypeUnset int = -1
)
// IndexedColorMapping is the table of default mappings from indexed color value
// to RGB value. Note that 0-7 are redundant of 8-15 to preserve backwards
// compatibility. A legacy indexing scheme for colors that is still required
// for some records, and for backwards compatibility with legacy formats. This
// element contains a sequence of RGB color values that correspond to color
// indexes (zero-based). When using the default indexed color palette, the
// values are not written out, but instead are implied. When the color palette
// has been modified from default, then the entire color palette is written
// out.
var IndexedColorMapping = []string{
"000000", "FFFFFF", "FF0000", "00FF00", "0000FF", "FFFF00", "FF00FF", "00FFFF",
"000000", "FFFFFF", "FF0000", "00FF00", "0000FF", "FFFF00", "FF00FF", "00FFFF",
"800000", "008000", "000080", "808000", "800080", "008080", "C0C0C0", "808080",
"9999FF", "993366", "FFFFCC", "CCFFFF", "660066", "FF8080", "0066CC", "CCCCFF",
"000080", "FF00FF", "FFFF00", "00FFFF", "800080", "800000", "008080", "0000FF",
"00CCFF", "CCFFFF", "CCFFCC", "FFFF99", "99CCFF", "FF99CC", "CC99FF", "FFCC99",
"3366FF", "33CCCC", "99CC00", "FFCC00", "FF9900", "FF6600", "666699", "969696",
"003366", "339966", "003300", "333300", "993300", "993366", "333399", "333333",
"000000", "FFFFFF",
}
// supportedImageTypes defined supported image types.
var supportedImageTypes = map[string]string{
".bmp": ".bmp", ".emf": ".emf", ".emz": ".emz", ".gif": ".gif",
".jpeg": ".jpeg", ".jpg": ".jpeg", ".png": ".png", ".svg": ".svg",
".tif": ".tiff", ".tiff": ".tiff", ".wmf": ".wmf", ".wmz": ".wmz",
}
// supportedContentTypes defined supported file format types.
var supportedContentTypes = map[string]string{
".xlam": ContentTypeAddinMacro,
".xlsm": ContentTypeMacro,
".xlsx": ContentTypeSheetML,
".xltm": ContentTypeTemplateMacro,
".xltx": ContentTypeTemplate,
}
// supportedUnderlineTypes defined supported underline types.
var supportedUnderlineTypes = []string{"none", "single", "double"}
// supportedDrawingUnderlineTypes defined supported underline types in drawing
// markup language.
var supportedDrawingUnderlineTypes = []string{
"none", "words", "sng", "dbl", "heavy", "dotted", "dottedHeavy", "dash", "dashHeavy", "dashLong", "dashLongHeavy", "dotDash", "dotDashHeavy", "dotDotDash", "dotDotDashHeavy", "wavy", "wavyHeavy",
"wavyDbl",
}
// xlsxCNvPr directly maps the cNvPr (Non-Visual Drawing Properties). This // xlsxCNvPr directly maps the cNvPr (Non-Visual Drawing Properties). This
// element specifies non-visual canvas properties. This allows for additional // element specifies non-visual canvas properties. This allows for additional
// information that does not affect the appearance of the picture to be stored. // information that does not affect the appearance of the picture to be stored.
@ -285,8 +83,8 @@ type xlsxOff struct {
Y int `xml:"y,attr"` Y int `xml:"y,attr"`
} }
// xlsxExt directly maps the ext element. // aExt directly maps the a:ext element.
type xlsxExt struct { type aExt struct {
Cx int `xml:"cx,attr"` Cx int `xml:"cx,attr"`
Cy int `xml:"cy,attr"` Cy int `xml:"cy,attr"`
} }
@ -305,7 +103,7 @@ type xlsxPrstGeom struct {
// be for a shape or group shape. // be for a shape or group shape.
type xlsxXfrm struct { type xlsxXfrm struct {
Off xlsxOff `xml:"a:off"` Off xlsxOff `xml:"a:off"`
Ext xlsxExt `xml:"a:ext"` Ext aExt `xml:"a:ext"`
} }
// xlsxCNvPicPr directly maps the cNvPicPr (Non-Visual Picture Drawing // xlsxCNvPicPr directly maps the cNvPicPr (Non-Visual Picture Drawing
@ -363,7 +161,8 @@ type xlsxBlipFill struct {
// has a minimum value of greater than or equal to 0. This simple type has a // has a minimum value of greater than or equal to 0. This simple type has a
// maximum value of less than or equal to 20116800. // maximum value of less than or equal to 20116800.
type xlsxLineProperties struct { type xlsxLineProperties struct {
W int `xml:"w,attr,omitempty"` W int `xml:"w,attr,omitempty"`
SolidFill *xlsxInnerXML `xml:"a:solidFill"`
} }
// xlsxSpPr directly maps the spPr (Shape Properties). This element specifies // xlsxSpPr directly maps the spPr (Shape Properties). This element specifies
@ -372,9 +171,10 @@ type xlsxLineProperties struct {
// but are used here to describe the visual appearance of a picture within a // but are used here to describe the visual appearance of a picture within a
// document. // document.
type xlsxSpPr struct { type xlsxSpPr struct {
Xfrm xlsxXfrm `xml:"a:xfrm"` Xfrm xlsxXfrm `xml:"a:xfrm"`
PrstGeom xlsxPrstGeom `xml:"a:prstGeom"` PrstGeom xlsxPrstGeom `xml:"a:prstGeom"`
Ln xlsxLineProperties `xml:"a:ln"` SolidFill *xlsxInnerXML `xml:"a:solidFill"`
Ln xlsxLineProperties `xml:"a:ln"`
} }
// xlsxPic elements encompass the definition of pictures within the DrawingML // xlsxPic elements encompass the definition of pictures within the DrawingML
@ -414,20 +214,53 @@ type xdrClientData struct {
FPrintsWithSheet bool `xml:"fPrintsWithSheet,attr"` FPrintsWithSheet bool `xml:"fPrintsWithSheet,attr"`
} }
// xdrCellAnchor directly maps the oneCellAnchor (One Cell Anchor Shape Size) // xdrCellAnchor specifies a oneCellAnchor (One Cell Anchor Shape Size) and
// and twoCellAnchor (Two Cell Anchor Shape Size). This element specifies a two // twoCellAnchor (Two Cell Anchor Shape Size) placeholder for a group, a shape,
// cell anchor placeholder for a group, a shape, or a drawing element. It moves // or a drawing element. It moves with cells and its extents are in EMU units.
// with cells and its extents are in EMU units.
type xdrCellAnchor struct { type xdrCellAnchor struct {
EditAs string `xml:"editAs,attr,omitempty"` EditAs string `xml:"editAs,attr,omitempty"`
Pos *xlsxPoint2D `xml:"xdr:pos"` Pos *xlsxPoint2D `xml:"xdr:pos"`
From *xlsxFrom `xml:"xdr:from"` From *xlsxFrom `xml:"xdr:from"`
To *xlsxTo `xml:"xdr:to"` To *xlsxTo `xml:"xdr:to"`
Ext *xlsxExt `xml:"xdr:ext"` Ext *aExt `xml:"xdr:ext"`
Sp *xdrSp `xml:"xdr:sp"` Sp *xdrSp `xml:"xdr:sp"`
Pic *xlsxPic `xml:"xdr:pic,omitempty"` Pic *xlsxPic `xml:"xdr:pic,omitempty"`
GraphicFrame string `xml:",innerxml"` GraphicFrame string `xml:",innerxml"`
ClientData *xdrClientData `xml:"xdr:clientData"` AlternateContent []*xlsxAlternateContent `xml:"mc:AlternateContent"`
ClientData *xdrClientData `xml:"xdr:clientData"`
}
// xlsxCellAnchorPos defines the structure used to serialize the cell anchor for
// adjust drawing object on inserting/deleting column/rows.
type xlsxCellAnchorPos struct {
EditAs string `xml:"editAs,attr,omitempty"`
From *xlsxFrom `xml:"xdr:from"`
To *xlsxTo `xml:"xdr:to"`
Pos *xlsxInnerXML `xml:"xdr:pos"`
Ext *xlsxInnerXML `xml:"xdr:ext"`
Sp *xlsxSp `xml:"xdr:sp"`
GrpSp *xlsxInnerXML `xml:"xdr:grpSp"`
GraphicFrame *xlsxInnerXML `xml:"xdr:graphicFrame"`
CxnSp *xlsxInnerXML `xml:"xdr:cxnSp"`
Pic *xlsxInnerXML `xml:"xdr:pic"`
ContentPart *xlsxInnerXML `xml:"xdr:contentPart"`
AlternateContent []*xlsxAlternateContent `xml:"mc:AlternateContent"`
ClientData *xlsxInnerXML `xml:"xdr:clientData"`
}
// xdrSp (Shape) directly maps the sp element. This element specifies the
// existence of a single shape. A shape can either be a preset or a custom
// geometry, defined using the SpreadsheetDrawingML framework. In addition to
// a geometry each shape can have both visual and non-visual properties
// attached. Text and corresponding styling information can also be attached
// to a shape. This shape is specified along with all other shapes within
// either the shape tree or group shape elements.
type xlsxSp struct {
Macro string `xml:"macro,attr,omitempty"`
TextLink string `xml:"textlink,attr,omitempty"`
FLocksText bool `xml:"fLocksText,attr,omitempty"`
FPublished *bool `xml:"fPublished,attr"`
Content string `xml:",innerxml"`
} }
// xlsxPoint2D describes the position of a drawing element within a spreadsheet. // xlsxPoint2D describes the position of a drawing element within a spreadsheet.
@ -440,8 +273,9 @@ type xlsxPoint2D struct {
// xlsxWsDr directly maps the root element for a part of this content type shall // xlsxWsDr directly maps the root element for a part of this content type shall
// wsDr. // wsDr.
type xlsxWsDr struct { type xlsxWsDr struct {
sync.Mutex mu sync.Mutex
XMLName xml.Name `xml:"xdr:wsDr"` XMLName xml.Name `xml:"xdr:wsDr"`
NS string `xml:"xmlns,attr,omitempty"`
A string `xml:"xmlns:a,attr,omitempty"` A string `xml:"xmlns:a,attr,omitempty"`
Xdr string `xml:"xmlns:xdr,attr,omitempty"` Xdr string `xml:"xmlns:xdr,attr,omitempty"`
R string `xml:"xmlns:r,attr,omitempty"` R string `xml:"xmlns:r,attr,omitempty"`
@ -491,6 +325,12 @@ type xlsxGraphic struct {
type xlsxGraphicData struct { type xlsxGraphicData struct {
URI string `xml:"uri,attr"` URI string `xml:"uri,attr"`
Chart *xlsxChart `xml:"c:chart,omitempty"` Chart *xlsxChart `xml:"c:chart,omitempty"`
Sle *xlsxSle `xml:"sle:slicer"`
}
type xlsxSle struct {
XMLNS string `xml:"xmlns:sle,attr"`
Name string `xml:"name,attr"`
} }
// xlsxChart (Chart) directly maps the c:chart element. // xlsxChart (Chart) directly maps the c:chart element.
@ -508,6 +348,7 @@ type xlsxChart struct {
// This shape is specified along with all other shapes within either the shape // This shape is specified along with all other shapes within either the shape
// tree or group shape elements. // tree or group shape elements.
type xdrSp struct { type xdrSp struct {
XMLName xml.Name `xml:"xdr:sp"`
Macro string `xml:"macro,attr"` Macro string `xml:"macro,attr"`
Textlink string `xml:"textlink,attr"` Textlink string `xml:"textlink,attr"`
NvSpPr *xdrNvSpPr `xml:"xdr:nvSpPr"` NvSpPr *xdrNvSpPr `xml:"xdr:nvSpPr"`
@ -583,29 +424,32 @@ type xdrTxBody struct {
// Picture maps the format settings of the picture. // Picture maps the format settings of the picture.
type Picture struct { type Picture struct {
Extension string Extension string
File []byte File []byte
Format *GraphicOptions Format *GraphicOptions
InsertType PictureInsertType
} }
// GraphicOptions directly maps the format settings of the picture. // GraphicOptions directly maps the format settings of the picture.
type GraphicOptions struct { type GraphicOptions struct {
AltText string AltText string
PrintObject *bool PrintObject *bool
Locked *bool Locked *bool
LockAspectRatio bool LockAspectRatio bool
AutoFit bool AutoFit bool
OffsetX int AutoFitIgnoreAspect bool
OffsetY int OffsetX int
ScaleX float64 OffsetY int
ScaleY float64 ScaleX float64
Hyperlink string ScaleY float64
HyperlinkType string Hyperlink string
Positioning string HyperlinkType string
Positioning string
} }
// Shape directly maps the format settings of the shape. // Shape directly maps the format settings of the shape.
type Shape struct { type Shape struct {
Cell string
Type string Type string
Macro string Macro string
Width uint Width uint
@ -616,13 +460,6 @@ type Shape struct {
Paragraph []RichTextRun Paragraph []RichTextRun
} }
// ShapeColor directly maps the color settings of the shape.
type ShapeColor struct {
Line string
Fill string
Effect string
}
// ShapeLine directly maps the line settings of the shape. // ShapeLine directly maps the line settings of the shape.
type ShapeLine struct { type ShapeLine struct {
Color string Color string

117
xmlMetaData.go Normal file
View File

@ -0,0 +1,117 @@
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
// Package excelize providing a set of functions that allow you to write to and
// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.18 or later.
package excelize
import "encoding/xml"
// xlsxMetadata directly maps the metadata element. A cell in a spreadsheet
// application can have metadata associated with it. Metadata is just a set of
// additional properties about the particular cell, and this metadata is stored
// in the metadata xml part. There are two types of metadata: cell metadata and
// value metadata. Cell metadata contains information about the cell itself,
// and this metadata can be carried along with the cell as it moves
// (insert, shift, copy/paste, merge, unmerge, etc). Value metadata is
// information about the value of a particular cell. Value metadata properties
// can be propagated along with the value as it is referenced in formulas.
type xlsxMetadata struct {
XMLName xml.Name `xml:"metadata"`
MetadataTypes *xlsxInnerXML `xml:"metadataTypes"`
MetadataStrings *xlsxInnerXML `xml:"metadataStrings"`
MdxMetadata *xlsxInnerXML `xml:"mdxMetadata"`
FutureMetadata []xlsxFutureMetadata `xml:"futureMetadata"`
CellMetadata *xlsxMetadataBlocks `xml:"cellMetadata"`
ValueMetadata *xlsxMetadataBlocks `xml:"valueMetadata"`
ExtLst *xlsxInnerXML `xml:"extLst"`
}
// xlsxFutureMetadata directly maps the futureMetadata element. This element
// represents future metadata information.
type xlsxFutureMetadata struct {
Bk []xlsxFutureMetadataBlock `xml:"bk"`
ExtLst *xlsxInnerXML `xml:"extLst"`
}
// xlsxFutureMetadataBlock directly maps the kb element. This element represents
// a block of future metadata information. This is a location for storing
// feature extension information.
type xlsxFutureMetadataBlock struct {
ExtLst *xlsxInnerXML `xml:"extLst"`
}
// xlsxMetadataBlocks directly maps the metadata element. This element
// represents cell metadata information. Cell metadata is information metadata
// about a specific cell, and it stays tied to that cell position.
type xlsxMetadataBlocks struct {
Count int `xml:"count,attr,omitempty"`
Bk []xlsxMetadataBlock `xml:"bk"`
}
// xlsxMetadataBlock directly maps the bk element. This element represents a
// block of metadata records.
type xlsxMetadataBlock struct {
Rc []xlsxMetadataRecord `xml:"rc"`
}
// xlsxMetadataRecord directly maps the rc element. This element represents a
// reference to a specific metadata record.
type xlsxMetadataRecord struct {
T int `xml:"t,attr"`
V int `xml:"v,attr"`
}
// xlsxRichValueData directly maps the rvData element that specifies rich value
// data.
type xlsxRichValueData struct {
XMLName xml.Name `xml:"rvData"`
Count int `xml:"count,attr,omitempty"`
Rv []xlsxRichValue `xml:"rv"`
ExtLst *xlsxInnerXML `xml:"extLst"`
}
// xlsxRichValue directly maps the rv element that specifies rich value data
// information for a single rich value
type xlsxRichValue struct {
S int `xml:"s,attr"`
V []string `xml:"v"`
Fb *xlsxInnerXML `xml:"fb"`
}
// xlsxRichValueRels directly maps the richValueRels element. This element that
// specifies a list of rich value relationships.
type xlsxRichValueRels struct {
XMLName xml.Name `xml:"richValueRels"`
Rels []xlsxRichValueRelRelationship `xml:"rel"`
ExtLst *xlsxInnerXML `xml:"extLst"`
}
// xlsxRichValueRelRelationship directly maps the rel element. This element
// specifies a relationship for a rich value property.
type xlsxRichValueRelRelationship struct {
ID string `xml:"id,attr"`
}
// xlsxWebImagesSupportingRichData directly maps the webImagesSrd element. This
// element specifies a list of sets of properties associated with web image rich
// values.
type xlsxWebImagesSupportingRichData struct {
XMLName xml.Name `xml:"webImagesSrd"`
WebImageSrd []xlsxWebImageSupportingRichData `xml:"webImageSrd"`
ExtLst *xlsxInnerXML `xml:"extLst"`
}
// xlsxWebImageSupportingRichData directly maps the webImageSrd element. This
// element specifies a set of properties for a web image rich value.
type xlsxWebImageSupportingRichData struct {
Address xlsxExternalReference `xml:"address"`
MoreImagesAddress xlsxExternalReference `xml:"moreImagesAddress"`
Blip xlsxExternalReference `xml:"blip"`
}

View File

@ -1,4 +1,4 @@
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in // this source code is governed by a BSD-style license that can be found in
// the LICENSE file. // the LICENSE file.
// //
@ -7,7 +7,7 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming // Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of // API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.16 or later. // data. This library needs Go version 1.18 or later.
package excelize package excelize
@ -120,26 +120,26 @@ type xlsxCacheField struct {
// those values that are referenced in multiple places across all the // those values that are referenced in multiple places across all the
// PivotTable parts. // PivotTable parts.
type xlsxSharedItems struct { type xlsxSharedItems struct {
ContainsSemiMixedTypes bool `xml:"containsSemiMixedTypes,attr,omitempty"` ContainsSemiMixedTypes bool `xml:"containsSemiMixedTypes,attr,omitempty"`
ContainsNonDate bool `xml:"containsNonDate,attr,omitempty"` ContainsNonDate bool `xml:"containsNonDate,attr,omitempty"`
ContainsDate bool `xml:"containsDate,attr,omitempty"` ContainsDate bool `xml:"containsDate,attr,omitempty"`
ContainsString bool `xml:"containsString,attr,omitempty"` ContainsString bool `xml:"containsString,attr,omitempty"`
ContainsBlank bool `xml:"containsBlank,attr,omitempty"` ContainsBlank bool `xml:"containsBlank,attr,omitempty"`
ContainsMixedTypes bool `xml:"containsMixedTypes,attr,omitempty"` ContainsMixedTypes bool `xml:"containsMixedTypes,attr,omitempty"`
ContainsNumber bool `xml:"containsNumber,attr,omitempty"` ContainsNumber bool `xml:"containsNumber,attr,omitempty"`
ContainsInteger bool `xml:"containsInteger,attr,omitempty"` ContainsInteger bool `xml:"containsInteger,attr,omitempty"`
MinValue float64 `xml:"minValue,attr,omitempty"` MinValue float64 `xml:"minValue,attr,omitempty"`
MaxValue float64 `xml:"maxValue,attr,omitempty"` MaxValue float64 `xml:"maxValue,attr,omitempty"`
MinDate string `xml:"minDate,attr,omitempty"` MinDate string `xml:"minDate,attr,omitempty"`
MaxDate string `xml:"maxDate,attr,omitempty"` MaxDate string `xml:"maxDate,attr,omitempty"`
Count int `xml:"count,attr"` Count int `xml:"count,attr"`
LongText bool `xml:"longText,attr,omitempty"` LongText bool `xml:"longText,attr,omitempty"`
M *xlsxMissing `xml:"m"` M []xlsxMissing `xml:"m"`
N *xlsxNumber `xml:"n"` N []xlsxNumber `xml:"n"`
B *xlsxBoolean `xml:"b"` B []xlsxBoolean `xml:"b"`
E *xlsxError `xml:"e"` E []xlsxError `xml:"e"`
S *xlsxString `xml:"s"` S []xlsxString `xml:"s"`
D *xlsxDateTime `xml:"d"` D []xlsxDateTime `xml:"d"`
} }
// xlsxMissing represents a value that was not specified. // xlsxMissing represents a value that was not specified.
@ -226,3 +226,17 @@ type xlsxMeasureGroups struct{}
// xlsxMaps represents the PivotTable OLAP measure group - Dimension maps. // xlsxMaps represents the PivotTable OLAP measure group - Dimension maps.
type xlsxMaps struct{} type xlsxMaps struct{}
// xlsxX14PivotCacheDefinition specifies the extended properties of a pivot
// table cache definition.
type xlsxX14PivotCacheDefinition struct {
XMLName xml.Name `xml:"x14:pivotCacheDefinition"`
PivotCacheID int `xml:"pivotCacheId,attr"`
}
// decodeX14PivotCacheDefinition defines the structure used to parse the
// x14:pivotCacheDefinition element of a pivot table cache.
type decodeX14PivotCacheDefinition struct {
XMLName xml.Name `xml:"pivotCacheDefinition"`
PivotCacheID int `xml:"pivotCacheId,attr"`
}

View File

@ -1,4 +1,4 @@
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in // this source code is governed by a BSD-style license that can be found in
// the LICENSE file. // the LICENSE file.
// //
@ -7,7 +7,7 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming // Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of // API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.16 or later. // data. This library needs Go version 1.18 or later.
package excelize package excelize
@ -56,15 +56,15 @@ type xlsxPivotTableDefinition struct {
EnableDrill bool `xml:"enableDrill,attr,omitempty"` EnableDrill bool `xml:"enableDrill,attr,omitempty"`
EnableFieldProperties bool `xml:"enableFieldProperties,attr,omitempty"` EnableFieldProperties bool `xml:"enableFieldProperties,attr,omitempty"`
PreserveFormatting bool `xml:"preserveFormatting,attr,omitempty"` PreserveFormatting bool `xml:"preserveFormatting,attr,omitempty"`
UseAutoFormatting *bool `xml:"useAutoFormatting,attr,omitempty"` UseAutoFormatting *bool `xml:"useAutoFormatting,attr"`
PageWrap int `xml:"pageWrap,attr,omitempty"` PageWrap int `xml:"pageWrap,attr,omitempty"`
PageOverThenDown *bool `xml:"pageOverThenDown,attr,omitempty"` PageOverThenDown *bool `xml:"pageOverThenDown,attr"`
SubtotalHiddenItems bool `xml:"subtotalHiddenItems,attr,omitempty"` SubtotalHiddenItems bool `xml:"subtotalHiddenItems,attr,omitempty"`
RowGrandTotals *bool `xml:"rowGrandTotals,attr,omitempty"` RowGrandTotals *bool `xml:"rowGrandTotals,attr"`
ColGrandTotals *bool `xml:"colGrandTotals,attr,omitempty"` ColGrandTotals *bool `xml:"colGrandTotals,attr"`
FieldPrintTitles bool `xml:"fieldPrintTitles,attr,omitempty"` FieldPrintTitles bool `xml:"fieldPrintTitles,attr,omitempty"`
ItemPrintTitles bool `xml:"itemPrintTitles,attr,omitempty"` ItemPrintTitles bool `xml:"itemPrintTitles,attr,omitempty"`
MergeItem *bool `xml:"mergeItem,attr,omitempty"` MergeItem *bool `xml:"mergeItem,attr"`
ShowDropZones bool `xml:"showDropZones,attr,omitempty"` ShowDropZones bool `xml:"showDropZones,attr,omitempty"`
CreatedVersion int `xml:"createdVersion,attr,omitempty"` CreatedVersion int `xml:"createdVersion,attr,omitempty"`
Indent int `xml:"indent,attr,omitempty"` Indent int `xml:"indent,attr,omitempty"`
@ -74,7 +74,7 @@ type xlsxPivotTableDefinition struct {
Compact *bool `xml:"compact,attr"` Compact *bool `xml:"compact,attr"`
Outline *bool `xml:"outline,attr"` Outline *bool `xml:"outline,attr"`
OutlineData bool `xml:"outlineData,attr,omitempty"` OutlineData bool `xml:"outlineData,attr,omitempty"`
CompactData *bool `xml:"compactData,attr,omitempty"` CompactData *bool `xml:"compactData,attr"`
Published bool `xml:"published,attr,omitempty"` Published bool `xml:"published,attr,omitempty"`
GridDropZones bool `xml:"gridDropZones,attr,omitempty"` GridDropZones bool `xml:"gridDropZones,attr,omitempty"`
Immersive bool `xml:"immersive,attr,omitempty"` Immersive bool `xml:"immersive,attr,omitempty"`
@ -150,7 +150,7 @@ type xlsxPivotField struct {
DataSourceSort bool `xml:"dataSourceSort,attr,omitempty"` DataSourceSort bool `xml:"dataSourceSort,attr,omitempty"`
NonAutoSortDefault bool `xml:"nonAutoSortDefault,attr,omitempty"` NonAutoSortDefault bool `xml:"nonAutoSortDefault,attr,omitempty"`
RankBy int `xml:"rankBy,attr,omitempty"` RankBy int `xml:"rankBy,attr,omitempty"`
DefaultSubtotal *bool `xml:"defaultSubtotal,attr,omitempty"` DefaultSubtotal *bool `xml:"defaultSubtotal,attr"`
SumSubtotal bool `xml:"sumSubtotal,attr,omitempty"` SumSubtotal bool `xml:"sumSubtotal,attr,omitempty"`
CountASubtotal bool `xml:"countASubtotal,attr,omitempty"` CountASubtotal bool `xml:"countASubtotal,attr,omitempty"`
AvgSubtotal bool `xml:"avgSubtotal,attr,omitempty"` AvgSubtotal bool `xml:"avgSubtotal,attr,omitempty"`
@ -273,7 +273,7 @@ type xlsxDataField struct {
ShowDataAs string `xml:"showDataAs,attr,omitempty"` ShowDataAs string `xml:"showDataAs,attr,omitempty"`
BaseField int `xml:"baseField,attr,omitempty"` BaseField int `xml:"baseField,attr,omitempty"`
BaseItem int64 `xml:"baseItem,attr,omitempty"` BaseItem int64 `xml:"baseItem,attr,omitempty"`
NumFmtID string `xml:"numFmtId,attr,omitempty"` NumFmtID int `xml:"numFmtId,attr,omitempty"`
ExtLst *xlsxExtLst `xml:"extLst"` ExtLst *xlsxExtLst `xml:"extLst"`
} }

View File

@ -1,4 +1,4 @@
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in // this source code is governed by a BSD-style license that can be found in
// the LICENSE file. // the LICENSE file.
// //
@ -7,11 +7,14 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming // Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of // API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.16 or later. // data. This library needs Go version 1.18 or later.
package excelize package excelize
import "encoding/xml" import (
"encoding/xml"
"sync"
)
// xlsxSST directly maps the sst element from the namespace // xlsxSST directly maps the sst element from the namespace
// http://schemas.openxmlformats.org/spreadsheetml/2006/main. String values may // http://schemas.openxmlformats.org/spreadsheetml/2006/main. String values may
@ -21,6 +24,7 @@ import "encoding/xml"
// is an indexed list of string values, shared across the workbook, which allows // is an indexed list of string values, shared across the workbook, which allows
// implementations to store values only once. // implementations to store values only once.
type xlsxSST struct { type xlsxSST struct {
mu sync.Mutex
XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main sst"` XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main sst"`
Count int `xml:"count,attr"` Count int `xml:"count,attr"`
UniqueCount int `xml:"uniqueCount,attr"` UniqueCount int `xml:"uniqueCount,attr"`

206
xmlSlicers.go Normal file
View File

@ -0,0 +1,206 @@
// Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//
// Package excelize providing a set of functions that allow you to write to and
// read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.18 or later.
package excelize
import "encoding/xml"
// xlsxSlicers directly maps the slicers element that specifies a slicer view on
// the worksheet.
type xlsxSlicers struct {
XMLName xml.Name `xml:"http://schemas.microsoft.com/office/spreadsheetml/2009/9/main slicers"`
XMLNSXMC string `xml:"xmlns:mc,attr"`
XMLNSX string `xml:"xmlns:x,attr"`
XMLNSXR10 string `xml:"xmlns:xr10,attr"`
Slicer []xlsxSlicer `xml:"slicer"`
}
// xlsxSlicer is a complex type that specifies a slicer view.
type xlsxSlicer struct {
Name string `xml:"name,attr"`
XR10UID string `xml:"xr10:uid,attr,omitempty"`
Cache string `xml:"cache,attr"`
Caption string `xml:"caption,attr,omitempty"`
StartItem *int `xml:"startItem,attr"`
ColumnCount *int `xml:"columnCount,attr"`
ShowCaption *bool `xml:"showCaption,attr"`
Level int `xml:"level,attr,omitempty"`
Style string `xml:"style,attr,omitempty"`
LockedPosition bool `xml:"lockedPosition,attr,omitempty"`
RowHeight int `xml:"rowHeight,attr"`
}
// slicerCacheDefinition directly maps the slicerCacheDefinition element that
// specifies a slicer cache.
type xlsxSlicerCacheDefinition struct {
XMLName xml.Name `xml:"http://schemas.microsoft.com/office/spreadsheetml/2009/9/main slicerCacheDefinition"`
XMLNSXMC string `xml:"xmlns:mc,attr"`
XMLNSX string `xml:"xmlns:x,attr"`
XMLNSX15 string `xml:"xmlns:x15,attr,omitempty"`
XMLNSXR10 string `xml:"xmlns:xr10,attr"`
Name string `xml:"name,attr"`
XR10UID string `xml:"xr10:uid,attr,omitempty"`
SourceName string `xml:"sourceName,attr"`
PivotTables *xlsxSlicerCachePivotTables `xml:"pivotTables"`
Data *xlsxSlicerCacheData `xml:"data"`
ExtLst *xlsxExtLst `xml:"extLst"`
}
// xlsxSlicerCachePivotTables is a complex type that specifies a group of
// pivotTable elements that specify the PivotTable views that are filtered by
// the slicer cache.
type xlsxSlicerCachePivotTables struct {
PivotTable []xlsxSlicerCachePivotTable `xml:"pivotTable"`
}
// xlsxSlicerCachePivotTable is a complex type that specifies a PivotTable view
// filtered by a slicer cache.
type xlsxSlicerCachePivotTable struct {
TabID int `xml:"tabId,attr"`
Name string `xml:"name,attr"`
}
// xlsxSlicerCacheData is a complex type that specifies a data source for the
// slicer cache.
type xlsxSlicerCacheData struct {
OLAP *xlsxInnerXML `xml:"olap"`
Tabular *xlsxTabularSlicerCache `xml:"tabular"`
}
// xlsxTabularSlicerCache is a complex type that specifies non-OLAP slicer items
// that are cached within this slicer cache and properties of the slicer cache
// specific to non-OLAP slicer items.
type xlsxTabularSlicerCache struct {
PivotCacheID int `xml:"pivotCacheId,attr"`
SortOrder string `xml:"sortOrder,attr,omitempty"`
CustomListSort *bool `xml:"customListSort,attr"`
ShowMissing *bool `xml:"showMissing,attr"`
CrossFilter string `xml:"crossFilter,attr,omitempty"`
Items *xlsxTabularSlicerCacheItems `xml:"items"`
ExtLst *xlsxExtLst `xml:"extLst"`
}
// xlsxTabularSlicerCacheItems is a complex type that specifies non-OLAP slicer
// items that are cached within this slicer cache.
type xlsxTabularSlicerCacheItems struct {
Count int `xml:"count,attr,omitempty"`
I []xlsxTabularSlicerCacheItem `xml:"i"`
}
// xlsxTabularSlicerCacheItem is a complex type that specifies a non-OLAP slicer
// item that is cached within this slicer cache.
type xlsxTabularSlicerCacheItem struct {
X int `xml:"x,attr"`
S bool `xml:"s,attr,omitempty"`
ND bool `xml:"nd,attr,omitempty"`
}
// xlsxTableSlicerCache specifies a table data source for the slicer cache.
type xlsxTableSlicerCache struct {
XMLName xml.Name `xml:"x15:tableSlicerCache"`
TableID int `xml:"tableId,attr"`
Column int `xml:"column,attr"`
SortOrder string `xml:"sortOrder,attr,omitempty"`
CustomListSort *bool `xml:"customListSort,attr"`
CrossFilter string `xml:"crossFilter,attr,omitempty"`
ExtLst *xlsxExtLst `xml:"extLst"`
}
// xlsxX14SlicerList specifies a list of slicer.
type xlsxX14SlicerList struct {
XMLName xml.Name `xml:"x14:slicerList"`
Slicer []*xlsxX14Slicer `xml:"x14:slicer"`
}
// xlsxX14Slicer specifies a slicer view,
type xlsxX14Slicer struct {
XMLName xml.Name `xml:"x14:slicer"`
RID string `xml:"r:id,attr"`
}
// xlsxX14SlicerCaches directly maps the x14:slicerCache element.
type xlsxX14SlicerCaches struct {
XMLName xml.Name `xml:"x14:slicerCaches"`
XMLNS string `xml:"xmlns:x14,attr"`
Content string `xml:",innerxml"`
}
// xlsxX15SlicerCaches directly maps the x14:slicerCache element.
type xlsxX14SlicerCache struct {
XMLName xml.Name `xml:"x14:slicerCache"`
RID string `xml:"r:id,attr"`
}
// xlsxX15SlicerCaches directly maps the x15:slicerCaches element.
type xlsxX15SlicerCaches struct {
XMLName xml.Name `xml:"x15:slicerCaches"`
XMLNS string `xml:"xmlns:x14,attr"`
Content string `xml:",innerxml"`
}
// decodeTableSlicerCache defines the structure used to parse the
// x15:tableSlicerCache element of the table slicer cache.
type decodeTableSlicerCache struct {
XMLName xml.Name `xml:"tableSlicerCache"`
TableID int `xml:"tableId,attr"`
Column int `xml:"column,attr"`
SortOrder string `xml:"sortOrder,attr"`
}
// decodeSlicerList defines the structure used to parse the x14:slicerList
// element of a list of slicer.
type decodeSlicerList struct {
XMLName xml.Name `xml:"slicerList"`
Slicer []*decodeSlicer `xml:"slicer"`
}
// decodeSlicer defines the structure used to parse the x14:slicer element of a
// slicer.
type decodeSlicer struct {
RID string `xml:"id,attr"`
}
// decodeSlicerCaches defines the structure used to parse the
// x14:slicerCaches and x15:slicerCaches element of a slicer cache.
type decodeSlicerCaches struct {
XMLName xml.Name `xml:"slicerCaches"`
Content string `xml:",innerxml"`
}
// xlsxTimelines is a mechanism for filtering data in pivot table views, cube
// functions and charts based on non-worksheet pivot tables. In the case of
// using OLAP Timeline source data, a Timeline is based on a key attribute of
// an OLAP hierarchy. In the case of using native Timeline source data, a
// Timeline is based on a data table column.
type xlsxTimelines struct {
XMLName xml.Name `xml:"http://schemas.microsoft.com/office/spreadsheetml/2010/11/main timelines"`
XMLNSXMC string `xml:"xmlns:mc,attr"`
XMLNSX string `xml:"xmlns:x,attr"`
XMLNSXR10 string `xml:"xmlns:xr10,attr"`
Timeline []xlsxTimeline `xml:"timeline"`
}
// xlsxTimeline is timeline view specifies the display of a timeline on a
// worksheet.
type xlsxTimeline struct {
Name string `xml:"name,attr"`
XR10UID string `xml:"xr10:uid,attr,omitempty"`
Cache string `xml:"cache,attr"`
Caption string `xml:"caption,attr,omitempty"`
ShowHeader *bool `xml:"showHeader,attr"`
ShowSelectionLabel *bool `xml:"showSelectionLabel,attr"`
ShowTimeLevel *bool `xml:"showTimeLevel,attr"`
ShowHorizontalScrollbar *bool `xml:"showHorizontalScrollbar,attr"`
Level int `xml:"level,attr"`
SelectionLevel int `xml:"selectionLevel,attr"`
ScrollPosition string `xml:"scrollPosition,attr,omitempty"`
Style string `xml:"style,attr,omitempty"`
}

View File

@ -1,4 +1,4 @@
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in // this source code is governed by a BSD-style license that can be found in
// the LICENSE file. // the LICENSE file.
// //
@ -7,7 +7,7 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming // Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of // API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.16 or later. // data. This library needs Go version 1.18 or later.
package excelize package excelize
@ -18,7 +18,7 @@ import (
// xlsxStyleSheet is the root element of the Styles part. // xlsxStyleSheet is the root element of the Styles part.
type xlsxStyleSheet struct { type xlsxStyleSheet struct {
sync.Mutex mu sync.Mutex
XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main styleSheet"` XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main styleSheet"`
NumFmts *xlsxNumFmts `xml:"numFmts"` NumFmts *xlsxNumFmts `xml:"numFmts"`
Fonts *xlsxFonts `xml:"fonts"` Fonts *xlsxFonts `xml:"fonts"`
@ -65,10 +65,10 @@ type xlsxLine struct {
// xlsxColor is a common mapping used for both the fgColor and bgColor elements. // xlsxColor is a common mapping used for both the fgColor and bgColor elements.
// Foreground color of the cell fill pattern. Cell fill patterns operate with // Foreground color of the cell fill pattern. Cell fill patterns operate with
// two colors: a background color and a foreground color. These combine together // two colors: a background color and a foreground color. These combine
// to make a patterned cell fill. Background color of the cell fill pattern. // to make a patterned cell fill. Background color of the cell fill pattern.
// Cell fill patterns operate with two colors: a background color and a // Cell fill patterns operate with two colors: a background color and a
// foreground color. These combine together to make a patterned cell fill. // foreground color. These combine to make a patterned cell fill.
type xlsxColor struct { type xlsxColor struct {
Auto bool `xml:"auto,attr,omitempty"` Auto bool `xml:"auto,attr,omitempty"`
RGB string `xml:"rgb,attr,omitempty"` RGB string `xml:"rgb,attr,omitempty"`
@ -103,7 +103,7 @@ type xlsxFont struct {
Scheme *attrValString `xml:"scheme"` Scheme *attrValString `xml:"scheme"`
} }
// xlsxFills directly maps the fills element. This element defines the cell // xlsxFills directly maps the fills' element. This element defines the cell
// fills portion of the Styles part, consisting of a sequence of fill records. A // fills portion of the Styles part, consisting of a sequence of fill records. A
// cell fill consists of a background color, foreground color, and pattern to be // cell fill consists of a background color, foreground color, and pattern to be
// applied across the cell. // applied across the cell.
@ -147,7 +147,7 @@ type xlsxGradientFillStop struct {
Color xlsxColor `xml:"color,omitempty"` Color xlsxColor `xml:"color,omitempty"`
} }
// xlsxBorders directly maps the borders element. This element contains borders // xlsxBorders directly maps the borders' element. This element contains borders
// formatting information, specifying all border definitions for all cells in // formatting information, specifying all border definitions for all cells in
// the workbook. // the workbook.
type xlsxBorders struct { type xlsxBorders struct {
@ -205,7 +205,7 @@ type xlsxCellStyleXfs struct {
Xf []xlsxXf `xml:"xf,omitempty"` Xf []xlsxXf `xml:"xf,omitempty"`
} }
// xlsxXf directly maps the xf element. A single xf element describes all of the // xlsxXf directly maps the xf element. A single xf element describes all the
// formatting for a cell. // formatting for a cell.
type xlsxXf struct { type xlsxXf struct {
NumFmtID *int `xml:"numFmtId,attr"` NumFmtID *int `xml:"numFmtId,attr"`
@ -236,8 +236,8 @@ type xlsxCellXfs struct {
} }
// xlsxDxfs directly maps the dxfs element. This element contains the master // xlsxDxfs directly maps the dxfs element. This element contains the master
// differential formatting records (dxf's) which define formatting for all non- // differential formatting records (dxf's) which define formatting for all
// cell formatting in this workbook. Whereas xf records fully specify a // non-cell formatting in this workbook. Whereas xf records fully specify a
// particular aspect of formatting (e.g., cell borders) by referencing those // particular aspect of formatting (e.g., cell borders) by referencing those
// formatting definitions elsewhere in the Styles part, dxf records specify // formatting definitions elsewhere in the Styles part, dxf records specify
// incremental (or differential) aspects of formatting directly inline within // incremental (or differential) aspects of formatting directly inline within
@ -251,18 +251,13 @@ type xlsxDxfs struct {
// xlsxDxf directly maps the dxf element. A single dxf record, expressing // xlsxDxf directly maps the dxf element. A single dxf record, expressing
// incremental formatting to be applied. // incremental formatting to be applied.
type xlsxDxf struct { type xlsxDxf struct {
Dxf string `xml:",innerxml"`
}
// dxf directly maps the dxf element.
type dxf struct {
Font *xlsxFont `xml:"font"` Font *xlsxFont `xml:"font"`
NumFmt *xlsxNumFmt `xml:"numFmt"` NumFmt *xlsxNumFmt `xml:"numFmt"`
Fill *xlsxFill `xml:"fill"` Fill *xlsxFill `xml:"fill"`
Alignment *xlsxAlignment `xml:"alignment"` Alignment *xlsxAlignment `xml:"alignment"`
Border *xlsxBorder `xml:"border"` Border *xlsxBorder `xml:"border"`
Protection *xlsxProtection `xml:"protection"` Protection *xlsxProtection `xml:"protection"`
ExtLst *xlsxExt `xml:"extLst"` ExtLst *aExt `xml:"extLst"`
} }
// xlsxTableStyles directly maps the tableStyles element. This element // xlsxTableStyles directly maps the tableStyles element. This element
@ -300,16 +295,24 @@ type xlsxNumFmts struct {
// format properties which indicate how to format and render the numeric value // format properties which indicate how to format and render the numeric value
// of a cell. // of a cell.
type xlsxNumFmt struct { type xlsxNumFmt struct {
NumFmtID int `xml:"numFmtId,attr"` NumFmtID int `xml:"numFmtId,attr"`
FormatCode string `xml:"formatCode,attr,omitempty"` FormatCode string `xml:"formatCode,attr"`
FormatCode16 string `xml:"http://schemas.microsoft.com/office/spreadsheetml/2015/02/main formatCode16,attr,omitempty"`
} }
// xlsxStyleColors directly maps the colors element. Color information // xlsxIndexedColors directly maps the single ARGB entry for the corresponding
// associated with this stylesheet. This collection is written whenever the // color index.
type xlsxIndexedColors struct {
RgbColor []xlsxColor `xml:"rgbColor"`
}
// xlsxStyleColors directly maps the colors' element. Color information
// associated with this style sheet. This collection is written whenever the
// legacy color palette has been modified (backwards compatibility settings) or // legacy color palette has been modified (backwards compatibility settings) or
// a custom color has been selected while using this workbook. // a custom color has been selected while using this workbook.
type xlsxStyleColors struct { type xlsxStyleColors struct {
Color string `xml:",innerxml"` IndexedColors *xlsxIndexedColors `xml:"indexedColors"`
MruColors *xlsxInnerXML `xml:"mruColors"`
} }
// Alignment directly maps the alignment settings of the cells. // Alignment directly maps the alignment settings of the cells.
@ -369,8 +372,7 @@ type Style struct {
Alignment *Alignment Alignment *Alignment
Protection *Protection Protection *Protection
NumFmt int NumFmt int
DecimalPlaces int DecimalPlaces *int
CustomNumFmt *string CustomNumFmt *string
Lang string
NegRed bool NegRed bool
} }

View File

@ -1,4 +1,4 @@
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in // this source code is governed by a BSD-style license that can be found in
// the LICENSE file. // the LICENSE file.
// //
@ -7,7 +7,7 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming // Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of // API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.16 or later. // data. This library needs Go version 1.18 or later.
package excelize package excelize
@ -21,22 +21,28 @@ import "encoding/xml"
type xlsxTable struct { type xlsxTable struct {
XMLName xml.Name `xml:"table"` XMLName xml.Name `xml:"table"`
XMLNS string `xml:"xmlns,attr"` XMLNS string `xml:"xmlns,attr"`
DataCellStyle string `xml:"dataCellStyle,attr,omitempty"`
DataDxfID int `xml:"dataDxfId,attr,omitempty"`
DisplayName string `xml:"displayName,attr,omitempty"`
HeaderRowBorderDxfID int `xml:"headerRowBorderDxfId,attr,omitempty"`
HeaderRowCellStyle string `xml:"headerRowCellStyle,attr,omitempty"`
HeaderRowCount *int `xml:"headerRowCount,attr"`
HeaderRowDxfID int `xml:"headerRowDxfId,attr,omitempty"`
ID int `xml:"id,attr"` ID int `xml:"id,attr"`
Name string `xml:"name,attr"`
DisplayName string `xml:"displayName,attr,omitempty"`
Comment string `xml:"comment,attr,omitempty"`
Ref string `xml:"ref,attr"`
TableType string `xml:"tableType,attr,omitempty"`
HeaderRowCount *int `xml:"headerRowCount,attr"`
InsertRow bool `xml:"insertRow,attr,omitempty"` InsertRow bool `xml:"insertRow,attr,omitempty"`
InsertRowShift bool `xml:"insertRowShift,attr,omitempty"` InsertRowShift bool `xml:"insertRowShift,attr,omitempty"`
Name string `xml:"name,attr"`
Published bool `xml:"published,attr,omitempty"`
Ref string `xml:"ref,attr"`
TotalsRowCount int `xml:"totalsRowCount,attr,omitempty"` TotalsRowCount int `xml:"totalsRowCount,attr,omitempty"`
TotalsRowShown *bool `xml:"totalsRowShown,attr"`
Published bool `xml:"published,attr,omitempty"`
HeaderRowDxfID int `xml:"headerRowDxfId,attr,omitempty"`
DataDxfID int `xml:"dataDxfId,attr,omitempty"`
TotalsRowDxfID int `xml:"totalsRowDxfId,attr,omitempty"` TotalsRowDxfID int `xml:"totalsRowDxfId,attr,omitempty"`
TotalsRowShown bool `xml:"totalsRowShown,attr"` HeaderRowBorderDxfID int `xml:"headerRowBorderDxfId,attr,omitempty"`
TableBorderDxfID int `xml:"tableBorderDxfId,attr,omitempty"`
TotalsRowBorderDxfID int `xml:"totalsRowBorderDxfId,attr,omitempty"`
HeaderRowCellStyle string `xml:"headerRowCellStyle,attr,omitempty"`
DataCellStyle string `xml:"dataCellStyle,attr,omitempty"`
TotalsRowCellStyle string `xml:"totalsRowCellStyle,attr,omitempty"`
ConnectionID int `xml:"connectionId,attr,omitempty"`
AutoFilter *xlsxAutoFilter `xml:"autoFilter"` AutoFilter *xlsxAutoFilter `xml:"autoFilter"`
TableColumns *xlsxTableColumns `xml:"tableColumns"` TableColumns *xlsxTableColumns `xml:"tableColumns"`
TableStyleInfo *xlsxTableStyleInfo `xml:"tableStyleInfo"` TableStyleInfo *xlsxTableStyleInfo `xml:"tableStyleInfo"`
@ -171,18 +177,18 @@ type xlsxTableColumns struct {
// xlsxTableColumn directly maps the element representing a single column for // xlsxTableColumn directly maps the element representing a single column for
// this table. // this table.
type xlsxTableColumn struct { type xlsxTableColumn struct {
DataCellStyle string `xml:"dataCellStyle,attr,omitempty"`
DataDxfID int `xml:"dataDxfId,attr,omitempty"`
HeaderRowCellStyle string `xml:"headerRowCellStyle,attr,omitempty"`
HeaderRowDxfID int `xml:"headerRowDxfId,attr,omitempty"`
ID int `xml:"id,attr"` ID int `xml:"id,attr"`
UniqueName string `xml:"uniqueName,attr,omitempty"`
Name string `xml:"name,attr"` Name string `xml:"name,attr"`
QueryTableFieldID int `xml:"queryTableFieldId,attr,omitempty"`
TotalsRowCellStyle string `xml:"totalsRowCellStyle,attr,omitempty"`
TotalsRowDxfID int `xml:"totalsRowDxfId,attr,omitempty"`
TotalsRowFunction string `xml:"totalsRowFunction,attr,omitempty"` TotalsRowFunction string `xml:"totalsRowFunction,attr,omitempty"`
TotalsRowLabel string `xml:"totalsRowLabel,attr,omitempty"` TotalsRowLabel string `xml:"totalsRowLabel,attr,omitempty"`
UniqueName string `xml:"uniqueName,attr,omitempty"` QueryTableFieldID int `xml:"queryTableFieldId,attr,omitempty"`
HeaderRowDxfID int `xml:"headerRowDxfId,attr,omitempty"`
DataDxfID int `xml:"dataDxfId,attr,omitempty"`
TotalsRowDxfID int `xml:"totalsRowDxfId,attr,omitempty"`
HeaderRowCellStyle string `xml:"headerRowCellStyle,attr,omitempty"`
DataCellStyle string `xml:"dataCellStyle,attr,omitempty"`
TotalsRowCellStyle string `xml:"totalsRowCellStyle,attr,omitempty"`
} }
// xlsxTableStyleInfo directly maps the tableStyleInfo element. This element // xlsxTableStyleInfo directly maps the tableStyleInfo element. This element
@ -196,8 +202,40 @@ type xlsxTableStyleInfo struct {
ShowColumnStripes bool `xml:"showColumnStripes,attr"` ShowColumnStripes bool `xml:"showColumnStripes,attr"`
} }
// xlsxSingleXMLCells is a single cell table is generated from an XML mapping.
// These really just look like regular cells to the spreadsheet user, but shall
// be implemented as Tables "under the covers."
type xlsxSingleXMLCells struct {
XMLName xml.Name `xml:"singleXmlCells"`
SingleXmlCell []xlsxSingleXMLCell `xml:"singleXmlCell"`
}
// xlsxSingleXMLCell is a element represents the table properties for a single
// cell XML table.
type xlsxSingleXMLCell struct {
XMLName xml.Name `xml:"singleXmlCell"`
ID int `xml:"id,attr"`
R string `xml:"r,attr"`
ConnectionID int `xml:"connectionId,attr"`
XMLCellPr xlsxXMLCellPr `xml:"xmlCellPr"`
ExtLst *xlsxInnerXML `xml:"extLst"`
}
// xlsxXMLCellPr is a element stores the XML properties for the cell of a single
// cell xml table.
type xlsxXMLCellPr struct {
XMLName xml.Name `xml:"xmlCellPr"`
ID int `xml:"id,attr"`
UniqueName string `xml:"uniqueName,attr,omitempty"`
XMLPr *xlsxInnerXML `xml:"xmlPr"`
ExtLst *xlsxInnerXML `xml:"extLst"`
}
// Table directly maps the format settings of the table. // Table directly maps the format settings of the table.
type Table struct { type Table struct {
tID int
rID string
tableXML string
Range string Range string
Name string Name string
StyleName string StyleName string

View File

@ -1,4 +1,4 @@
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in // this source code is governed by a BSD-style license that can be found in
// the LICENSE file. // the LICENSE file.
// //
@ -7,7 +7,7 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming // Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of // API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.16 or later. // data. This library needs Go version 1.18 or later.
package excelize package excelize
@ -16,15 +16,15 @@ import "encoding/xml"
// xlsxTheme directly maps the theme element in the namespace // xlsxTheme directly maps the theme element in the namespace
// http://schemas.openxmlformats.org/drawingml/2006/main // http://schemas.openxmlformats.org/drawingml/2006/main
type xlsxTheme struct { type xlsxTheme struct {
XMLName xml.Name `xml:"http://schemas.openxmlformats.org/drawingml/2006/main theme"` XMLName xml.Name `xml:"a:theme"`
XMLNSa string `xml:"xmlns:a,attr"` XMLNSa string `xml:"xmlns:a,attr"`
XMLNSr string `xml:"xmlns:r,attr"` XMLNSr string `xml:"xmlns:r,attr"`
Name string `xml:"name,attr"` Name string `xml:"name,attr"`
ThemeElements xlsxBaseStyles `xml:"themeElements"` ThemeElements xlsxBaseStyles `xml:"a:themeElements"`
ObjectDefaults xlsxObjectDefaults `xml:"objectDefaults"` ObjectDefaults xlsxObjectDefaults `xml:"a:objectDefaults"`
ExtraClrSchemeLst xlsxExtraClrSchemeLst `xml:"extraClrSchemeLst"` ExtraClrSchemeLst xlsxExtraClrSchemeLst `xml:"a:extraClrSchemeLst"`
CustClrLst *xlsxInnerXML `xml:"custClrLst"` CustClrLst *xlsxInnerXML `xml:"a:custClrLst"`
ExtLst *xlsxExtLst `xml:"extLst"` ExtLst *xlsxExtLst `xml:"a:extLst"`
} }
// xlsxBaseStyles defines the theme elements for a theme, and is the workhorse // xlsxBaseStyles defines the theme elements for a theme, and is the workhorse
@ -33,40 +33,40 @@ type xlsxTheme struct {
// scheme, a font scheme, and a style matrix (format scheme) that defines // scheme, a font scheme, and a style matrix (format scheme) that defines
// different formatting options for different pieces of a document. // different formatting options for different pieces of a document.
type xlsxBaseStyles struct { type xlsxBaseStyles struct {
ClrScheme xlsxColorScheme `xml:"clrScheme"` ClrScheme xlsxColorScheme `xml:"a:clrScheme"`
FontScheme xlsxFontScheme `xml:"fontScheme"` FontScheme xlsxFontScheme `xml:"a:fontScheme"`
FmtScheme xlsxStyleMatrix `xml:"fmtScheme"` FmtScheme xlsxStyleMatrix `xml:"a:fmtScheme"`
ExtLst *xlsxExtLst `xml:"extLst"` ExtLst *xlsxExtLst `xml:"a:extLst"`
} }
// xlsxCTColor holds the actual color values that are to be applied to a given // xlsxCTColor holds the actual color values that are to be applied to a given
// diagram and how those colors are to be applied. // diagram and how those colors are to be applied.
type xlsxCTColor struct { type xlsxCTColor struct {
ScrgbClr *xlsxInnerXML `xml:"scrgbClr"` ScrgbClr *xlsxInnerXML `xml:"a:scrgbClr"`
SrgbClr *attrValString `xml:"srgbClr"` SrgbClr *attrValString `xml:"a:srgbClr"`
HslClr *xlsxInnerXML `xml:"hslClr"` HslClr *xlsxInnerXML `xml:"a:hslClr"`
SysClr *xlsxSysClr `xml:"sysClr"` SysClr *xlsxSysClr `xml:"a:sysClr"`
SchemeClr *xlsxInnerXML `xml:"schemeClr"` SchemeClr *xlsxInnerXML `xml:"a:schemeClr"`
PrstClr *xlsxInnerXML `xml:"prstClr"` PrstClr *xlsxInnerXML `xml:"a:prstClr"`
} }
// xlsxColorScheme defines a set of colors for the theme. The set of colors // xlsxColorScheme defines a set of colors for the theme. The set of colors
// consists of twelve color slots that can each hold a color of choice. // consists of twelve color slots that can each hold a color of choice.
type xlsxColorScheme struct { type xlsxColorScheme struct {
Name string `xml:"name,attr"` Name string `xml:"name,attr"`
Dk1 xlsxCTColor `xml:"dk1"` Dk1 xlsxCTColor `xml:"a:dk1"`
Lt1 xlsxCTColor `xml:"lt1"` Lt1 xlsxCTColor `xml:"a:lt1"`
Dk2 xlsxCTColor `xml:"dk2"` Dk2 xlsxCTColor `xml:"a:dk2"`
Lt2 xlsxCTColor `xml:"lt2"` Lt2 xlsxCTColor `xml:"a:lt2"`
Accent1 xlsxCTColor `xml:"accent1"` Accent1 xlsxCTColor `xml:"a:accent1"`
Accent2 xlsxCTColor `xml:"accent2"` Accent2 xlsxCTColor `xml:"a:accent2"`
Accent3 xlsxCTColor `xml:"accent3"` Accent3 xlsxCTColor `xml:"a:accent3"`
Accent4 xlsxCTColor `xml:"accent4"` Accent4 xlsxCTColor `xml:"a:accent4"`
Accent5 xlsxCTColor `xml:"accent5"` Accent5 xlsxCTColor `xml:"a:accent5"`
Accent6 xlsxCTColor `xml:"accent6"` Accent6 xlsxCTColor `xml:"a:accent6"`
Hlink xlsxCTColor `xml:"hlink"` Hlink xlsxCTColor `xml:"a:hlink"`
FolHlink xlsxCTColor `xml:"folHlink"` FolHlink xlsxCTColor `xml:"a:folHlink"`
ExtLst *xlsxExtLst `xml:"extLst"` ExtLst *xlsxExtLst `xml:"a:extLst"`
} }
// objectDefaults element allows for the definition of default shape, line, // objectDefaults element allows for the definition of default shape, line,
@ -95,11 +95,11 @@ type xlsxCTSupplementalFont struct {
// Asian, and complex script. On top of these three definitions, one can also // Asian, and complex script. On top of these three definitions, one can also
// define a font for use in a specific language or languages. // define a font for use in a specific language or languages.
type xlsxFontCollection struct { type xlsxFontCollection struct {
Latin *xlsxCTTextFont `xml:"latin"` Latin *xlsxCTTextFont `xml:"a:latin"`
Ea *xlsxCTTextFont `xml:"ea"` Ea *xlsxCTTextFont `xml:"a:ea"`
Cs *xlsxCTTextFont `xml:"cs"` Cs *xlsxCTTextFont `xml:"a:cs"`
Font []xlsxCTSupplementalFont `xml:"font"` Font []xlsxCTSupplementalFont `xml:"a:font"`
ExtLst *xlsxExtLst `xml:"extLst"` ExtLst *xlsxExtLst `xml:"a:extLst"`
} }
// xlsxFontScheme element defines the font scheme within the theme. The font // xlsxFontScheme element defines the font scheme within the theme. The font
@ -109,9 +109,9 @@ type xlsxFontCollection struct {
// paragraph areas. // paragraph areas.
type xlsxFontScheme struct { type xlsxFontScheme struct {
Name string `xml:"name,attr"` Name string `xml:"name,attr"`
MajorFont xlsxFontCollection `xml:"majorFont"` MajorFont xlsxFontCollection `xml:"a:majorFont"`
MinorFont xlsxFontCollection `xml:"minorFont"` MinorFont xlsxFontCollection `xml:"a:minorFont"`
ExtLst *xlsxExtLst `xml:"extLst"` ExtLst *xlsxExtLst `xml:"a:extLst"`
} }
// xlsxStyleMatrix defines a set of formatting options, which can be referenced // xlsxStyleMatrix defines a set of formatting options, which can be referenced
@ -121,10 +121,10 @@ type xlsxFontScheme struct {
// change when the theme is changed. // change when the theme is changed.
type xlsxStyleMatrix struct { type xlsxStyleMatrix struct {
Name string `xml:"name,attr,omitempty"` Name string `xml:"name,attr,omitempty"`
FillStyleLst xlsxFillStyleLst `xml:"fillStyleLst"` FillStyleLst xlsxFillStyleLst `xml:"a:fillStyleLst"`
LnStyleLst xlsxLnStyleLst `xml:"lnStyleLst"` LnStyleLst xlsxLnStyleLst `xml:"a:lnStyleLst"`
EffectStyleLst xlsxEffectStyleLst `xml:"effectStyleLst"` EffectStyleLst xlsxEffectStyleLst `xml:"a:effectStyleLst"`
BgFillStyleLst xlsxBgFillStyleLst `xml:"bgFillStyleLst"` BgFillStyleLst xlsxBgFillStyleLst `xml:"a:bgFillStyleLst"`
} }
// xlsxFillStyleLst element defines a set of three fill styles that are used // xlsxFillStyleLst element defines a set of three fill styles that are used
@ -148,7 +148,7 @@ type xlsxEffectStyleLst struct {
EffectStyleLst string `xml:",innerxml"` EffectStyleLst string `xml:",innerxml"`
} }
// xlsxBgFillStyleLst element defines a list of background fills that are // xlsxBgFillStyleLst element defines a list of background fills that are
// used within a theme. The background fills consist of three fills, arranged // used within a theme. The background fills consist of three fills, arranged
// in order from subtle to moderate to intense. // in order from subtle to moderate to intense.
type xlsxBgFillStyleLst struct { type xlsxBgFillStyleLst struct {
@ -161,3 +161,85 @@ type xlsxSysClr struct {
Val string `xml:"val,attr"` Val string `xml:"val,attr"`
LastClr string `xml:"lastClr,attr"` LastClr string `xml:"lastClr,attr"`
} }
// decodeTheme defines the structure used to parse the a:theme element for the
// theme.
type decodeTheme struct {
XMLName xml.Name `xml:"http://schemas.openxmlformats.org/drawingml/2006/main theme"`
Name string `xml:"name,attr"`
ThemeElements decodeBaseStyles `xml:"themeElements"`
ObjectDefaults xlsxObjectDefaults `xml:"objectDefaults"`
ExtraClrSchemeLst xlsxExtraClrSchemeLst `xml:"extraClrSchemeLst"`
CustClrLst *xlsxInnerXML `xml:"custClrLst"`
ExtLst *xlsxExtLst `xml:"extLst"`
}
// decodeBaseStyles defines the structure used to parse the theme elements for a
// theme, and is the workhorse of the theme.
type decodeBaseStyles struct {
ClrScheme decodeColorScheme `xml:"clrScheme"`
FontScheme decodeFontScheme `xml:"fontScheme"`
FmtScheme decodeStyleMatrix `xml:"fmtScheme"`
ExtLst *xlsxExtLst `xml:"extLst"`
}
// decodeColorScheme defines the structure used to parse a set of colors for the
// theme.
type decodeColorScheme struct {
Name string `xml:"name,attr"`
Dk1 decodeCTColor `xml:"dk1"`
Lt1 decodeCTColor `xml:"lt1"`
Dk2 decodeCTColor `xml:"dk2"`
Lt2 decodeCTColor `xml:"lt2"`
Accent1 decodeCTColor `xml:"accent1"`
Accent2 decodeCTColor `xml:"accent2"`
Accent3 decodeCTColor `xml:"accent3"`
Accent4 decodeCTColor `xml:"accent4"`
Accent5 decodeCTColor `xml:"accent5"`
Accent6 decodeCTColor `xml:"accent6"`
Hlink decodeCTColor `xml:"hlink"`
FolHlink decodeCTColor `xml:"folHlink"`
ExtLst *xlsxExtLst `xml:"extLst"`
}
// decodeFontScheme defines the structure used to parse font scheme within the
// theme.
type decodeFontScheme struct {
Name string `xml:"name,attr"`
MajorFont decodeFontCollection `xml:"majorFont"`
MinorFont decodeFontCollection `xml:"minorFont"`
ExtLst *xlsxExtLst `xml:"extLst"`
}
// decodeFontCollection defines the structure used to parse a major and minor
// font which is used in the font scheme.
type decodeFontCollection struct {
Latin *xlsxCTTextFont `xml:"latin"`
Ea *xlsxCTTextFont `xml:"ea"`
Cs *xlsxCTTextFont `xml:"cs"`
Font []xlsxCTSupplementalFont `xml:"font"`
ExtLst *xlsxExtLst `xml:"extLst"`
}
// decodeCTColor defines the structure used to parse the actual color values
// that are to be applied to a given diagram and how those colors are to be
// applied.
type decodeCTColor struct {
ScrgbClr *xlsxInnerXML `xml:"scrgbClr"`
SrgbClr *attrValString `xml:"srgbClr"`
HslClr *xlsxInnerXML `xml:"hslClr"`
SysClr *xlsxSysClr `xml:"sysClr"`
SchemeClr *xlsxInnerXML `xml:"schemeClr"`
PrstClr *xlsxInnerXML `xml:"prstClr"`
}
// decodeStyleMatrix defines the structure used to parse a set of formatting
// options, which can be referenced by documents that apply a certain style to
// a given part of an object.
type decodeStyleMatrix struct {
Name string `xml:"name,attr,omitempty"`
FillStyleLst xlsxFillStyleLst `xml:"fillStyleLst"`
LnStyleLst xlsxLnStyleLst `xml:"lnStyleLst"`
EffectStyleLst xlsxEffectStyleLst `xml:"effectStyleLst"`
BgFillStyleLst xlsxBgFillStyleLst `xml:"bgFillStyleLst"`
}

View File

@ -1,4 +1,4 @@
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in // this source code is governed by a BSD-style license that can be found in
// the LICENSE file. // the LICENSE file.
// //
@ -7,7 +7,7 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming // Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of // API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.16 or later. // data. This library needs Go version 1.18 or later.
package excelize package excelize
@ -16,9 +16,10 @@ import (
"sync" "sync"
) )
// xlsxRelationships describe references from parts to other internal resources in the package or to external resources. // xlsxRelationships describe references from parts to other internal resources
// in the package or to external resources.
type xlsxRelationships struct { type xlsxRelationships struct {
sync.Mutex mu sync.Mutex
XMLName xml.Name `xml:"http://schemas.openxmlformats.org/package/2006/relationships Relationships"` XMLName xml.Name `xml:"http://schemas.openxmlformats.org/package/2006/relationships Relationships"`
Relationships []xlsxRelationship `xml:"Relationship"` Relationships []xlsxRelationship `xml:"Relationship"`
} }
@ -141,19 +142,19 @@ type xlsxBookViews struct {
// http://schemas.openxmlformats.org/spreadsheetml/2006/main This element // http://schemas.openxmlformats.org/spreadsheetml/2006/main This element
// specifies a single Workbook view. // specifies a single Workbook view.
type xlsxWorkBookView struct { type xlsxWorkBookView struct {
Visibility string `xml:"visibility,attr,omitempty"` Visibility string `xml:"visibility,attr,omitempty"`
Minimized bool `xml:"minimized,attr,omitempty"` Minimized bool `xml:"minimized,attr,omitempty"`
ShowHorizontalScroll *bool `xml:"showHorizontalScroll,attr"` ShowHorizontalScroll *bool `xml:"showHorizontalScroll,attr"`
ShowVerticalScroll *bool `xml:"showVerticalScroll,attr"` ShowVerticalScroll *bool `xml:"showVerticalScroll,attr"`
ShowSheetTabs *bool `xml:"showSheetTabs,attr"` ShowSheetTabs *bool `xml:"showSheetTabs,attr"`
XWindow string `xml:"xWindow,attr,omitempty"` XWindow string `xml:"xWindow,attr,omitempty"`
YWindow string `xml:"yWindow,attr,omitempty"` YWindow string `xml:"yWindow,attr,omitempty"`
WindowWidth int `xml:"windowWidth,attr,omitempty"` WindowWidth int `xml:"windowWidth,attr,omitempty"`
WindowHeight int `xml:"windowHeight,attr,omitempty"` WindowHeight int `xml:"windowHeight,attr,omitempty"`
TabRatio int `xml:"tabRatio,attr,omitempty"` TabRatio float64 `xml:"tabRatio,attr,omitempty"`
FirstSheet int `xml:"firstSheet,attr,omitempty"` FirstSheet int `xml:"firstSheet,attr,omitempty"`
ActiveTab int `xml:"activeTab,attr,omitempty"` ActiveTab int `xml:"activeTab,attr,omitempty"`
AutoFilterDateGrouping *bool `xml:"autoFilterDateGrouping,attr"` AutoFilterDateGrouping *bool `xml:"autoFilterDateGrouping,attr"`
} }
// xlsxSheets directly maps the sheets element from the namespace // xlsxSheets directly maps the sheets element from the namespace
@ -212,12 +213,70 @@ type xlsxPivotCache struct {
// document are specified in the markup specification and can be used to store // document are specified in the markup specification and can be used to store
// extensions to the markup specification, whether those are future version // extensions to the markup specification, whether those are future version
// extensions of the markup specification or are private extensions implemented // extensions of the markup specification or are private extensions implemented
// independently from the markup specification. Markup within an extension might // independently of the markup specification. Markup within an extension might
// not be understood by a consumer. // not be understood by a consumer.
type xlsxExtLst struct { type xlsxExtLst struct {
Ext string `xml:",innerxml"` Ext string `xml:",innerxml"`
} }
// xlsxExt represents a the future feature data storage area. Each extension
// within an extension list shall be contained within an ext element.
// Extensions shall be versioned by namespace, using the uri attribute, and
// shall be allowed to appear in any order within the extension list. Any
// number of extensions shall be allowed within an extension list.
type xlsxExt struct {
XMLName xml.Name `xml:"ext"`
URI string `xml:"uri,attr"`
Content string `xml:",innerxml"`
xmlns []xml.Attr
}
// xlsxAlternateContent is a container for a sequence of multiple
// representations of a given piece of content. The program reading the file
// should only process one of these, and the one chosen should be based on
// which conditions match.
type xlsxAlternateContent struct {
XMLNSMC string `xml:"xmlns:mc,attr,omitempty"`
Content string `xml:",innerxml"`
}
// xlsxChoice element shall be an element in the Markup Compatibility namespace
// with local name "Choice". Parent elements of Choice elements shall be
// AlternateContent elements.
type xlsxChoice struct {
XMLName xml.Name `xml:"mc:Choice"`
XMLNSA14 string `xml:"xmlns:a14,attr,omitempty"`
XMLNSSle15 string `xml:"xmlns:sle15,attr,omitempty"`
Requires string `xml:"Requires,attr,omitempty"`
Content string `xml:",innerxml"`
}
// xlsxFallback element shall be an element in the Markup Compatibility
// namespace with local name "Fallback". Parent elements of Fallback elements
// shall be AlternateContent elements.
type xlsxFallback struct {
XMLName xml.Name `xml:"mc:Fallback"`
Content string `xml:",innerxml"`
}
// xlsxInnerXML holds parts of XML content currently not unmarshal.
type xlsxInnerXML struct {
Content string `xml:",innerxml"`
}
// decodeExtLst defines the structure used to parse the extLst element
// of the future feature data storage area.
type decodeExtLst struct {
XMLName xml.Name `xml:"extLst"`
Ext []*xlsxExt `xml:"ext"`
}
// decodeExt defines the structure used to parse the ext element.
type decodeExt struct {
URI string `xml:"uri,attr,omitempty"`
Content string `xml:",innerxml"`
}
// xlsxDefinedNames directly maps the definedNames element. This element defines // xlsxDefinedNames directly maps the definedNames element. This element defines
// the collection of defined names for this workbook. Defined names are // the collection of defined names for this workbook. Defined names are
// descriptive names to represent cells, ranges of cells, formulas, or constant // descriptive names to represent cells, ranges of cells, formulas, or constant
@ -229,7 +288,7 @@ type xlsxDefinedNames struct {
// xlsxDefinedName directly maps the definedName element from the namespace // xlsxDefinedName directly maps the definedName element from the namespace
// http://schemas.openxmlformats.org/spreadsheetml/2006/main This element // http://schemas.openxmlformats.org/spreadsheetml/2006/main This element
// defines a defined name within this workbook. A defined name is descriptive // defines a defined name within this workbook. A defined name is descriptive
// text that is used to represents a cell, range of cells, formula, or constant // text that is used to represent a cell, range of cells, formula, or constant
// value. For a descriptions of the attributes see https://learn.microsoft.com/en-us/dotnet/api/documentformat.openxml.spreadsheet.definedname // value. For a descriptions of the attributes see https://learn.microsoft.com/en-us/dotnet/api/documentformat.openxml.spreadsheet.definedname
type xlsxDefinedName struct { type xlsxDefinedName struct {
Comment string `xml:"comment,attr,omitempty"` Comment string `xml:"comment,attr,omitempty"`
@ -290,30 +349,30 @@ type xlsxCustomWorkbookViews struct {
// to implement configurable display modes, the customWorkbookView element // to implement configurable display modes, the customWorkbookView element
// should be used to persist the settings for those display modes. // should be used to persist the settings for those display modes.
type xlsxCustomWorkbookView struct { type xlsxCustomWorkbookView struct {
ActiveSheetID *int `xml:"activeSheetId,attr"` ActiveSheetID *int `xml:"activeSheetId,attr"`
AutoUpdate *bool `xml:"autoUpdate,attr"` AutoUpdate *bool `xml:"autoUpdate,attr"`
ChangesSavedWin *bool `xml:"changesSavedWin,attr"` ChangesSavedWin *bool `xml:"changesSavedWin,attr"`
GUID *string `xml:"guid,attr"` GUID *string `xml:"guid,attr"`
IncludeHiddenRowCol *bool `xml:"includeHiddenRowCol,attr"` IncludeHiddenRowCol *bool `xml:"includeHiddenRowCol,attr"`
IncludePrintSettings *bool `xml:"includePrintSettings,attr"` IncludePrintSettings *bool `xml:"includePrintSettings,attr"`
Maximized *bool `xml:"maximized,attr"` Maximized *bool `xml:"maximized,attr"`
MergeInterval int `xml:"mergeInterval,attr"` MergeInterval int `xml:"mergeInterval,attr"`
Minimized *bool `xml:"minimized,attr"` Minimized *bool `xml:"minimized,attr"`
Name *string `xml:"name,attr"` Name *string `xml:"name,attr"`
OnlySync *bool `xml:"onlySync,attr"` OnlySync *bool `xml:"onlySync,attr"`
PersonalView *bool `xml:"personalView,attr"` PersonalView *bool `xml:"personalView,attr"`
ShowComments *string `xml:"showComments,attr"` ShowComments *string `xml:"showComments,attr"`
ShowFormulaBar *bool `xml:"showFormulaBar,attr"` ShowFormulaBar *bool `xml:"showFormulaBar,attr"`
ShowHorizontalScroll *bool `xml:"showHorizontalScroll,attr"` ShowHorizontalScroll *bool `xml:"showHorizontalScroll,attr"`
ShowObjects *string `xml:"showObjects,attr"` ShowObjects *string `xml:"showObjects,attr"`
ShowSheetTabs *bool `xml:"showSheetTabs,attr"` ShowSheetTabs *bool `xml:"showSheetTabs,attr"`
ShowStatusbar *bool `xml:"showStatusbar,attr"` ShowStatusbar *bool `xml:"showStatusbar,attr"`
ShowVerticalScroll *bool `xml:"showVerticalScroll,attr"` ShowVerticalScroll *bool `xml:"showVerticalScroll,attr"`
TabRatio *int `xml:"tabRatio,attr"` TabRatio *float64 `xml:"tabRatio,attr"`
WindowHeight *int `xml:"windowHeight,attr"` WindowHeight *int `xml:"windowHeight,attr"`
WindowWidth *int `xml:"windowWidth,attr"` WindowWidth *int `xml:"windowWidth,attr"`
XWindow *int `xml:"xWindow,attr"` XWindow *int `xml:"xWindow,attr"`
YWindow *int `xml:"yWindow,attr"` YWindow *int `xml:"yWindow,attr"`
} }
// DefinedName directly maps the name for a cell or cell range on a // DefinedName directly maps the name for a cell or cell range on a

View File

@ -1,4 +1,4 @@
// Copyright 2016 - 2023 The excelize Authors. All rights reserved. Use of // Copyright 2016 - 2024 The excelize Authors. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in // this source code is governed by a BSD-style license that can be found in
// the LICENSE file. // the LICENSE file.
// //
@ -7,7 +7,7 @@
// writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. // writing spreadsheet documents generated by Microsoft Excel™ 2007 and later.
// Supports complex components by high compatibility, and provided streaming // Supports complex components by high compatibility, and provided streaming
// API for generating or reading data from a worksheet with huge amounts of // API for generating or reading data from a worksheet with huge amounts of
// data. This library needs Go version 1.16 or later. // data. This library needs Go version 1.18 or later.
package excelize package excelize
@ -19,7 +19,7 @@ import (
// xlsxWorksheet directly maps the worksheet element in the namespace // xlsxWorksheet directly maps the worksheet element in the namespace
// http://schemas.openxmlformats.org/spreadsheetml/2006/main. // http://schemas.openxmlformats.org/spreadsheetml/2006/main.
type xlsxWorksheet struct { type xlsxWorksheet struct {
sync.Mutex mu sync.Mutex
XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main worksheet"` XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main worksheet"`
SheetPr *xlsxSheetPr `xml:"sheetPr"` SheetPr *xlsxSheetPr `xml:"sheetPr"`
Dimension *xlsxDimension `xml:"dimension"` Dimension *xlsxDimension `xml:"dimension"`
@ -81,8 +81,8 @@ type xlsxHeaderFooter struct {
XMLName xml.Name `xml:"headerFooter"` XMLName xml.Name `xml:"headerFooter"`
DifferentOddEven bool `xml:"differentOddEven,attr,omitempty"` DifferentOddEven bool `xml:"differentOddEven,attr,omitempty"`
DifferentFirst bool `xml:"differentFirst,attr,omitempty"` DifferentFirst bool `xml:"differentFirst,attr,omitempty"`
ScaleWithDoc bool `xml:"scaleWithDoc,attr,omitempty"` ScaleWithDoc *bool `xml:"scaleWithDoc,attr"`
AlignWithMargins bool `xml:"alignWithMargins,attr,omitempty"` AlignWithMargins *bool `xml:"alignWithMargins,attr"`
OddHeader string `xml:"oddHeader,omitempty"` OddHeader string `xml:"oddHeader,omitempty"`
OddFooter string `xml:"oddFooter,omitempty"` OddFooter string `xml:"oddFooter,omitempty"`
EvenHeader string `xml:"evenHeader,omitempty"` EvenHeader string `xml:"evenHeader,omitempty"`
@ -241,7 +241,7 @@ type xlsxSheetPr struct {
CodeName string `xml:"codeName,attr,omitempty"` CodeName string `xml:"codeName,attr,omitempty"`
FilterMode bool `xml:"filterMode,attr,omitempty"` FilterMode bool `xml:"filterMode,attr,omitempty"`
EnableFormatConditionsCalculation *bool `xml:"enableFormatConditionsCalculation,attr"` EnableFormatConditionsCalculation *bool `xml:"enableFormatConditionsCalculation,attr"`
TabColor *xlsxTabColor `xml:"tabColor"` TabColor *xlsxColor `xml:"tabColor"`
OutlinePr *xlsxOutlinePr `xml:"outlinePr"` OutlinePr *xlsxOutlinePr `xml:"outlinePr"`
PageSetUpPr *xlsxPageSetUpPr `xml:"pageSetUpPr"` PageSetUpPr *xlsxPageSetUpPr `xml:"pageSetUpPr"`
} }
@ -261,15 +261,6 @@ type xlsxPageSetUpPr struct {
FitToPage bool `xml:"fitToPage,attr,omitempty"` FitToPage bool `xml:"fitToPage,attr,omitempty"`
} }
// xlsxTabColor represents background color of the sheet tab.
type xlsxTabColor struct {
Auto bool `xml:"auto,attr,omitempty"`
Indexed int `xml:"indexed,attr,omitempty"`
RGB string `xml:"rgb,attr,omitempty"`
Theme int `xml:"theme,attr,omitempty"`
Tint float64 `xml:"tint,attr,omitempty"`
}
// xlsxCols defines column width and column formatting for one or more columns // xlsxCols defines column width and column formatting for one or more columns
// of the worksheet. // of the worksheet.
type xlsxCols struct { type xlsxCols struct {
@ -428,31 +419,32 @@ type xlsxMergeCells struct {
// xlsxDataValidations expresses all data validation information for cells in a // xlsxDataValidations expresses all data validation information for cells in a
// sheet which have data validation features applied. // sheet which have data validation features applied.
type xlsxDataValidations struct { type xlsxDataValidations struct {
XMLName xml.Name `xml:"dataValidations"` XMLName xml.Name `xml:"dataValidations"`
Count int `xml:"count,attr,omitempty"` Count int `xml:"count,attr,omitempty"`
DisablePrompts bool `xml:"disablePrompts,attr,omitempty"` DisablePrompts bool `xml:"disablePrompts,attr,omitempty"`
XWindow int `xml:"xWindow,attr,omitempty"` XWindow int `xml:"xWindow,attr,omitempty"`
YWindow int `xml:"yWindow,attr,omitempty"` YWindow int `xml:"yWindow,attr,omitempty"`
DataValidation []*DataValidation `xml:"dataValidation"` DataValidation []*xlsxDataValidation `xml:"dataValidation"`
} }
// DataValidation directly maps the a single item of data validation defined // DataValidation directly maps the single item of data validation defined
// on a range of the worksheet. // on a range of the worksheet.
type DataValidation struct { type xlsxDataValidation struct {
AllowBlank bool `xml:"allowBlank,attr"` AllowBlank bool `xml:"allowBlank,attr"`
Error *string `xml:"error,attr"` Error *string `xml:"error,attr"`
ErrorStyle *string `xml:"errorStyle,attr"` ErrorStyle *string `xml:"errorStyle,attr"`
ErrorTitle *string `xml:"errorTitle,attr"` ErrorTitle *string `xml:"errorTitle,attr"`
Operator string `xml:"operator,attr,omitempty"` Operator string `xml:"operator,attr,omitempty"`
Prompt *string `xml:"prompt,attr"` Prompt *string `xml:"prompt,attr"`
PromptTitle *string `xml:"promptTitle,attr"` PromptTitle *string `xml:"promptTitle,attr"`
ShowDropDown bool `xml:"showDropDown,attr,omitempty"` ShowDropDown bool `xml:"showDropDown,attr,omitempty"`
ShowErrorMessage bool `xml:"showErrorMessage,attr,omitempty"` ShowErrorMessage bool `xml:"showErrorMessage,attr,omitempty"`
ShowInputMessage bool `xml:"showInputMessage,attr,omitempty"` ShowInputMessage bool `xml:"showInputMessage,attr,omitempty"`
Sqref string `xml:"sqref,attr"` Sqref string `xml:"sqref,attr"`
Type string `xml:"type,attr,omitempty"` XMSqref string `xml:"sqref,omitempty"`
Formula1 string `xml:",innerxml"` Type string `xml:"type,attr,omitempty"`
Formula2 string `xml:",innerxml"` Formula1 *xlsxInnerXML `xml:"formula1"`
Formula2 *xlsxInnerXML `xml:"formula2"`
} }
// xlsxC collection represents a cell in the worksheet. Information about the // xlsxC collection represents a cell in the worksheet. Information about the
@ -486,6 +478,7 @@ type xlsxC struct {
F *xlsxF `xml:"f"` // Formula F *xlsxF `xml:"f"` // Formula
V string `xml:"v,omitempty"` // Value V string `xml:"v,omitempty"` // Value
IS *xlsxSI `xml:"is"` IS *xlsxSI `xml:"is"`
f string
} }
// xlsxF represents a formula for the cell. The formula expression is // xlsxF represents a formula for the cell. The formula expression is
@ -708,33 +701,6 @@ type xlsxLegacyDrawingHF struct {
RID string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr,omitempty"` RID string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr,omitempty"`
} }
// xlsxAlternateContent is a container for a sequence of multiple
// representations of a given piece of content. The program reading the file
// should only process one of these, and the one chosen should be based on
// which conditions match.
type xlsxAlternateContent struct {
XMLNSMC string `xml:"xmlns:mc,attr,omitempty"`
Content string `xml:",innerxml"`
}
// xlsxInnerXML holds parts of XML content currently not unmarshal.
type xlsxInnerXML struct {
Content string `xml:",innerxml"`
}
// xlsxWorksheetExt directly maps the ext element in the worksheet.
type xlsxWorksheetExt struct {
XMLName xml.Name `xml:"ext"`
URI string `xml:"uri,attr"`
Content string `xml:",innerxml"`
}
// decodeWorksheetExt directly maps the ext element.
type decodeWorksheetExt struct {
XMLName xml.Name `xml:"extLst"`
Ext []*xlsxWorksheetExt `xml:"ext"`
}
// decodeX14SparklineGroups directly maps the sparklineGroups element. // decodeX14SparklineGroups directly maps the sparklineGroups element.
type decodeX14SparklineGroups struct { type decodeX14SparklineGroups struct {
XMLName xml.Name `xml:"sparklineGroups"` XMLName xml.Name `xml:"sparklineGroups"`
@ -742,8 +708,7 @@ type decodeX14SparklineGroups struct {
Content string `xml:",innerxml"` Content string `xml:",innerxml"`
} }
// decodeX14ConditionalFormattingExt directly maps the ext // decodeX14ConditionalFormattingExt directly maps the ext element.
// element.
type decodeX14ConditionalFormattingExt struct { type decodeX14ConditionalFormattingExt struct {
XMLName xml.Name `xml:"ext"` XMLName xml.Name `xml:"ext"`
ID string `xml:"id"` ID string `xml:"id"`
@ -757,6 +722,14 @@ type decodeX14ConditionalFormattings struct {
Content string `xml:",innerxml"` Content string `xml:",innerxml"`
} }
// decodeX14ConditionalFormattingRules directly maps the conditionalFormattings
// element.
type decodeX14ConditionalFormattingRules struct {
XMLName xml.Name `xml:"conditionalFormattings"`
XMLNSXM string `xml:"xmlns:xm,attr"`
CondFmt []decodeX14ConditionalFormatting `xml:"conditionalFormatting"`
}
// decodeX14ConditionalFormatting directly maps the conditionalFormatting // decodeX14ConditionalFormatting directly maps the conditionalFormatting
// element. // element.
type decodeX14ConditionalFormatting struct { type decodeX14ConditionalFormatting struct {
@ -778,7 +751,7 @@ type decodeX14DataBar struct {
MaxLength int `xml:"maxLength,attr"` MaxLength int `xml:"maxLength,attr"`
MinLength int `xml:"minLength,attr"` MinLength int `xml:"minLength,attr"`
Border bool `xml:"border,attr,omitempty"` Border bool `xml:"border,attr,omitempty"`
Gradient bool `xml:"gradient,attr"` Gradient *bool `xml:"gradient,attr"`
ShowValue bool `xml:"showValue,attr,omitempty"` ShowValue bool `xml:"showValue,attr,omitempty"`
Direction string `xml:"direction,attr,omitempty"` Direction string `xml:"direction,attr,omitempty"`
Cfvo []*xlsxCfvo `xml:"cfvo"` Cfvo []*xlsxCfvo `xml:"cfvo"`
@ -850,14 +823,14 @@ type xlsxX14SparklineGroup struct {
MinAxisType string `xml:"minAxisType,attr,omitempty"` MinAxisType string `xml:"minAxisType,attr,omitempty"`
MaxAxisType string `xml:"maxAxisType,attr,omitempty"` MaxAxisType string `xml:"maxAxisType,attr,omitempty"`
RightToLeft bool `xml:"rightToLeft,attr,omitempty"` RightToLeft bool `xml:"rightToLeft,attr,omitempty"`
ColorSeries *xlsxTabColor `xml:"x14:colorSeries"` ColorSeries *xlsxColor `xml:"x14:colorSeries"`
ColorNegative *xlsxTabColor `xml:"x14:colorNegative"` ColorNegative *xlsxColor `xml:"x14:colorNegative"`
ColorAxis *xlsxColor `xml:"x14:colorAxis"` ColorAxis *xlsxColor `xml:"x14:colorAxis"`
ColorMarkers *xlsxTabColor `xml:"x14:colorMarkers"` ColorMarkers *xlsxColor `xml:"x14:colorMarkers"`
ColorFirst *xlsxTabColor `xml:"x14:colorFirst"` ColorFirst *xlsxColor `xml:"x14:colorFirst"`
ColorLast *xlsxTabColor `xml:"x14:colorLast"` ColorLast *xlsxColor `xml:"x14:colorLast"`
ColorHigh *xlsxTabColor `xml:"x14:colorHigh"` ColorHigh *xlsxColor `xml:"x14:colorHigh"`
ColorLow *xlsxTabColor `xml:"x14:colorLow"` ColorLow *xlsxColor `xml:"x14:colorLow"`
Sparklines xlsxX14Sparklines `xml:"x14:sparklines"` Sparklines xlsxX14Sparklines `xml:"x14:sparklines"`
} }
@ -872,6 +845,24 @@ type xlsxX14Sparkline struct {
Sqref string `xml:"xm:sqref"` Sqref string `xml:"xm:sqref"`
} }
// DataValidation directly maps the settings of the data validation rule.
type DataValidation struct {
AllowBlank bool
Error *string
ErrorStyle *string
ErrorTitle *string
Operator string
Prompt *string
PromptTitle *string
ShowDropDown bool
ShowErrorMessage bool
ShowInputMessage bool
Sqref string
Type string
Formula1 string
Formula2 string
}
// SparklineOptions directly maps the settings of the sparkline. // SparklineOptions directly maps the settings of the sparkline.
type SparklineOptions struct { type SparklineOptions struct {
Location []string Location []string
@ -903,8 +894,8 @@ type SparklineOptions struct {
EmptyCells string EmptyCells string
} }
// PaneOptions directly maps the settings of the pane. // Selection directly maps the settings of the worksheet selection.
type PaneOptions struct { type Selection struct {
SQRef string SQRef string
ActiveCell string ActiveCell string
Pane string Pane string
@ -918,7 +909,7 @@ type Panes struct {
YSplit int YSplit int
TopLeftCell string TopLeftCell string
ActivePane string ActivePane string
Panes []PaneOptions Selection []Selection
} }
// ConditionalFormatOptions directly maps the conditional format settings of the cells. // ConditionalFormatOptions directly maps the conditional format settings of the cells.
@ -926,7 +917,7 @@ type ConditionalFormatOptions struct {
Type string Type string
AboveAverage bool AboveAverage bool
Percent bool Percent bool
Format int Format *int
Criteria string Criteria string
Value string Value string
MinType string MinType string
@ -972,10 +963,10 @@ type SheetProtectionOptions struct {
// HeaderFooterOptions directly maps the settings of header and footer. // HeaderFooterOptions directly maps the settings of header and footer.
type HeaderFooterOptions struct { type HeaderFooterOptions struct {
AlignWithMargins bool AlignWithMargins *bool
DifferentFirst bool DifferentFirst bool
DifferentOddEven bool DifferentOddEven bool
ScaleWithDoc bool ScaleWithDoc *bool
OddHeader string OddHeader string
OddFooter string OddFooter string
EvenHeader string EvenHeader string
@ -1015,6 +1006,9 @@ type PageLayoutOptions struct {
FitToWidth *int FitToWidth *int
// BlackAndWhite specified print black and white. // BlackAndWhite specified print black and white.
BlackAndWhite *bool BlackAndWhite *bool
// PageOrder specifies the ordering of multiple pages. Values
// accepted: overThenDown, downThenOver
PageOrder *string
} }
// ViewOptions directly maps the settings of sheet view. // ViewOptions directly maps the settings of sheet view.