Compare commits

...

155 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
87 changed files with 23156 additions and 4694 deletions

View File

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

View File

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

View File

@ -1,6 +1,6 @@
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
All rights reserved.

View File

@ -13,7 +13,7 @@
## 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
@ -165,8 +165,10 @@ func main() {
Categories: "Sheet1!$B$1:$D$1",
Values: "Sheet1!$B$4:$D$4",
}},
Title: excelize.ChartTitle{
Name: "Fruit 3D Clustered Column Chart",
Title: []excelize.RichTextRun{
{
Text: "Fruit 3D Clustered Column Chart",
},
},
}); err != nil {
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",
Values: "Sheet1!$B$4:$D$4",
}},
Title: excelize.ChartTitle{
Name: "Fruit 3D Clustered Column Chart",
Title: []excelize.RichTextRun{
{
Text: "Fruit 3D Clustered Column Chart",
},
},
}); err != nil {
fmt.Println(err)

891
adjust.go

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,14 @@
package excelize
import (
"encoding/xml"
"fmt"
"path/filepath"
"strings"
"testing"
_ "image/jpeg"
"github.com/stretchr/testify/assert"
)
@ -19,7 +23,7 @@ func TestAdjustMergeCells(t *testing.T) {
},
},
},
}, rows, 0, 0), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")))
}, "Sheet1", rows, 0, 0, 1), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")))
assert.Equal(t, f.adjustMergeCells(&xlsxWorksheet{
MergeCells: &xlsxMergeCells{
Cells: []*xlsxMergeCell{
@ -28,7 +32,7 @@ func TestAdjustMergeCells(t *testing.T) {
},
},
},
}, rows, 0, 0), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")))
}, "Sheet1", rows, 0, 0, 1), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")))
assert.NoError(t, f.adjustMergeCells(&xlsxWorksheet{
MergeCells: &xlsxMergeCells{
Cells: []*xlsxMergeCell{
@ -37,7 +41,7 @@ func TestAdjustMergeCells(t *testing.T) {
},
},
},
}, rows, 1, -1))
}, "Sheet1", rows, 1, -1, 1))
assert.NoError(t, f.adjustMergeCells(&xlsxWorksheet{
MergeCells: &xlsxMergeCells{
Cells: []*xlsxMergeCell{
@ -46,7 +50,7 @@ func TestAdjustMergeCells(t *testing.T) {
},
},
},
}, columns, 1, -1))
}, "Sheet1", columns, 1, -1, 1))
assert.NoError(t, f.adjustMergeCells(&xlsxWorksheet{
MergeCells: &xlsxMergeCells{
Cells: []*xlsxMergeCell{
@ -55,7 +59,7 @@ func TestAdjustMergeCells(t *testing.T) {
},
},
},
}, columns, 1, -1))
}, "Sheet1", columns, 1, -1, 1))
// Test adjust merge cells
var cases []struct {
@ -134,7 +138,7 @@ func TestAdjustMergeCells(t *testing.T) {
},
}
for _, c := range cases {
assert.NoError(t, f.adjustMergeCells(c.ws, c.dir, c.num, 1))
assert.NoError(t, f.adjustMergeCells(c.ws, "Sheet1", c.dir, c.num, 1, 1))
assert.Equal(t, c.expect, c.ws.MergeCells.Cells[0].Ref, c.label)
assert.Equal(t, c.expectRect, c.ws.MergeCells.Cells[0].rect, c.label)
}
@ -223,7 +227,7 @@ func TestAdjustMergeCells(t *testing.T) {
},
}
for _, c := range cases {
assert.NoError(t, f.adjustMergeCells(c.ws, c.dir, c.num, -1))
assert.NoError(t, f.adjustMergeCells(c.ws, "Sheet1", c.dir, c.num, -1, 1))
assert.Equal(t, c.expect, c.ws.MergeCells.Cells[0].Ref, c.label)
}
@ -271,7 +275,7 @@ func TestAdjustMergeCells(t *testing.T) {
},
}
for _, c := range cases {
assert.NoError(t, f.adjustMergeCells(c.ws, c.dir, c.num, -1))
assert.NoError(t, f.adjustMergeCells(c.ws, "Sheet1", c.dir, c.num, -1, 1))
assert.Len(t, c.ws.MergeCells.Cells, 0, c.label)
}
@ -291,18 +295,18 @@ func TestAdjustAutoFilter(t *testing.T) {
AutoFilter: &xlsxAutoFilter{
Ref: "A1:A3",
},
}, rows, 1, -1))
}, "Sheet1", rows, 1, -1, 1))
// Test adjustAutoFilter with illegal cell reference
assert.Equal(t, f.adjustAutoFilter(&xlsxWorksheet{
AutoFilter: &xlsxAutoFilter{
Ref: "A:B1",
},
}, rows, 0, 0), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")))
}, "Sheet1", rows, 0, 0, 1), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")))
assert.Equal(t, f.adjustAutoFilter(&xlsxWorksheet{
AutoFilter: &xlsxAutoFilter{
Ref: "A1:B",
},
}, rows, 0, 0), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")))
}, "Sheet1", rows, 0, 0, 1), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")))
}
func TestAdjustTable(t *testing.T) {
@ -331,10 +335,10 @@ func TestAdjustTable(t *testing.T) {
assert.NoError(t, f.RemoveRow(sheetName, 1))
// Test adjust table with unsupported charset
f.Pkg.Store("xl/tables/table1.xml", MacintoshCyrillicCharset)
assert.NoError(t, f.RemoveRow(sheetName, 1))
assert.EqualError(t, f.RemoveRow(sheetName, 1), "XML syntax error on line 1: invalid UTF-8")
// Test adjust table with invalid table range reference
f.Pkg.Store("xl/tables/table1.xml", []byte(`<table ref="-" />`))
assert.NoError(t, f.RemoveRow(sheetName, 1))
assert.Equal(t, ErrParameterInvalid, f.RemoveRow(sheetName, 1))
}
func TestAdjustHelper(t *testing.T) {
@ -357,13 +361,18 @@ func TestAdjustHelper(t *testing.T) {
func TestAdjustCalcChain(t *testing.T) {
f := NewFile()
f.CalcChain = &xlsxCalcChain{
C: []xlsxCalcChainC{
{R: "B2", I: 2}, {R: "B2", I: 1},
},
C: []xlsxCalcChainC{{R: "B2", I: 2}, {R: "B2", I: 1}, {R: "A1", I: 1}},
}
assert.NoError(t, f.InsertCols("Sheet1", "A", 1))
assert.NoError(t, f.InsertRows("Sheet1", 1, 1))
f.CalcChain = &xlsxCalcChain{
C: []xlsxCalcChainC{{R: "B2", I: 1}, {R: "B3"}, {R: "A1"}},
}
assert.NoError(t, f.RemoveRow("Sheet1", 3))
assert.NoError(t, f.RemoveCol("Sheet1", "B"))
f.CalcChain = &xlsxCalcChain{C: []xlsxCalcChainC{{R: "B2", I: 2}, {R: "B2", I: 1}}}
f.CalcChain.C[1].R = "invalid coordinates"
assert.Equal(t, f.InsertCols("Sheet1", "A", 1), newCellNameToCoordinatesError("invalid coordinates", newInvalidCellNameError("invalid coordinates")))
f.CalcChain = nil
@ -444,16 +453,112 @@ func TestAdjustCols(t *testing.T) {
assert.NoError(t, f.RemoveCol(sheetName, "A"))
assert.NoError(t, f.Close())
f = NewFile()
assert.NoError(t, f.SetColWidth("Sheet1", "XFB", "XFC", 12))
assert.NoError(t, f.InsertCols("Sheet1", "A", 2))
ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml")
assert.True(t, ok)
assert.Equal(t, MaxColumns, ws.(*xlsxWorksheet).Cols.Col[0].Min)
assert.Equal(t, MaxColumns, ws.(*xlsxWorksheet).Cols.Col[0].Max)
assert.NoError(t, f.InsertCols("Sheet1", "A", 2))
assert.Nil(t, ws.(*xlsxWorksheet).Cols)
f = NewFile()
assert.NoError(t, f.SetCellFormula("Sheet1", "A2", "(1-0.5)/2"))
assert.NoError(t, f.InsertCols("Sheet1", "A", 1))
formula, err := f.GetCellFormula("Sheet1", "B2")
assert.NoError(t, err)
assert.Equal(t, "(1-0.5)/2", formula)
assert.NoError(t, f.Close())
}
func TestAdjustColDimensions(t *testing.T) {
f := NewFile()
ws, err := f.workSheetReader("Sheet1")
assert.NoError(t, err)
assert.NoError(t, f.SetCellFormula("Sheet1", "C3", "A1+B1"))
assert.Equal(t, ErrColumnNumber, f.adjustColDimensions("Sheet1", ws, 1, MaxColumns))
_, err = f.NewSheet("Sheet2")
assert.NoError(t, err)
f.Sheet.Delete("xl/worksheets/sheet2.xml")
f.Pkg.Store("xl/worksheets/sheet2.xml", MacintoshCyrillicCharset)
assert.EqualError(t, f.adjustColDimensions("Sheet2", ws, 2, 1), "XML syntax error on line 1: invalid UTF-8")
}
func TestAdjustRowDimensions(t *testing.T) {
f := NewFile()
ws, err := f.workSheetReader("Sheet1")
assert.NoError(t, err)
assert.NoError(t, f.SetCellFormula("Sheet1", "C3", "A1+B1"))
assert.Equal(t, ErrMaxRows, f.adjustRowDimensions("Sheet1", ws, 1, TotalRows))
_, err = f.NewSheet("Sheet2")
assert.NoError(t, err)
f.Sheet.Delete("xl/worksheets/sheet2.xml")
f.Pkg.Store("xl/worksheets/sheet2.xml", MacintoshCyrillicCharset)
assert.EqualError(t, f.adjustRowDimensions("Sheet1", ws, 2, 1), "XML syntax error on line 1: invalid UTF-8")
f = NewFile()
_, err = f.NewSheet("Sheet2")
assert.NoError(t, err)
ws, err = f.workSheetReader("Sheet1")
assert.NoError(t, err)
assert.NoError(t, f.SetCellFormula("Sheet1", "B2", fmt.Sprintf("Sheet2!A%d", TotalRows)))
assert.Equal(t, ErrMaxRows, f.adjustRowDimensions("Sheet2", ws, 1, TotalRows))
}
func TestAdjustHyperlinks(t *testing.T) {
f := NewFile()
ws, err := f.workSheetReader("Sheet1")
assert.NoError(t, err)
assert.NoError(t, f.SetCellFormula("Sheet1", "C3", "A1+B1"))
f.adjustHyperlinks(ws, "Sheet1", rows, 3, -1)
// Test adjust hyperlinks location with positive offset
assert.NoError(t, f.SetCellHyperLink("Sheet1", "F5", "Sheet1!A1", "Location"))
assert.NoError(t, f.InsertRows("Sheet1", 1, 1))
link, target, err := f.GetCellHyperLink("Sheet1", "F6")
assert.NoError(t, err)
assert.True(t, link)
assert.Equal(t, target, "Sheet1!A1")
// Test adjust hyperlinks location with negative offset
assert.NoError(t, f.RemoveRow("Sheet1", 1))
link, target, err = f.GetCellHyperLink("Sheet1", "F5")
assert.NoError(t, err)
assert.True(t, link)
assert.Equal(t, target, "Sheet1!A1")
// Test adjust hyperlinks location on remove row
assert.NoError(t, f.RemoveRow("Sheet1", 5))
link, target, err = f.GetCellHyperLink("Sheet1", "F5")
assert.NoError(t, err)
assert.False(t, link)
assert.Empty(t, target)
// Test adjust hyperlinks location on remove column
assert.NoError(t, f.SetCellHyperLink("Sheet1", "F5", "Sheet1!A1", "Location"))
assert.NoError(t, f.RemoveCol("Sheet1", "F"))
link, target, err = f.GetCellHyperLink("Sheet1", "F5")
assert.NoError(t, err)
assert.False(t, link)
assert.Empty(t, target)
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAdjustHyperlinks.xlsx")))
assert.NoError(t, f.Close())
}
func TestAdjustFormula(t *testing.T) {
f := NewFile()
formulaType, ref := STCellFormulaTypeShared, "C1:C5"
assert.NoError(t, f.SetCellFormula("Sheet1", "C1", "=A1+B1", FormulaOpts{Ref: &ref, Type: &formulaType}))
assert.NoError(t, f.SetCellFormula("Sheet1", "C1", "A1+B1", FormulaOpts{Ref: &ref, Type: &formulaType}))
assert.NoError(t, f.DuplicateRowTo("Sheet1", 1, 10))
assert.NoError(t, f.InsertCols("Sheet1", "B", 1))
assert.NoError(t, f.InsertRows("Sheet1", 1, 1))
for cell, expected := range map[string]string{"D2": "=A1+B1", "D3": "=A2+B2", "D11": "=A1+B1"} {
for cell, expected := range map[string]string{"D2": "A2+C2", "D3": "A3+C3", "D11": "A11+C11"} {
formula, err := f.GetCellFormula("Sheet1", cell)
assert.NoError(t, err)
assert.Equal(t, expected, formula)
@ -461,7 +566,750 @@ func TestAdjustFormula(t *testing.T) {
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAdjustFormula.xlsx")))
assert.NoError(t, f.Close())
assert.NoError(t, f.adjustFormula(nil, rows, 0, false))
assert.Equal(t, f.adjustFormula(&xlsxF{Ref: "-"}, rows, 0, false), ErrParameterInvalid)
assert.Equal(t, f.adjustFormula(&xlsxF{Ref: "XFD1:XFD1"}, columns, 1, false), ErrColumnNumber)
assert.NoError(t, f.adjustFormula("Sheet1", "Sheet1", &xlsxC{}, rows, 0, 0, false))
assert.Equal(t, newCellNameToCoordinatesError("-", newInvalidCellNameError("-")), f.adjustFormula("Sheet1", "Sheet1", &xlsxC{F: &xlsxF{Ref: "-"}}, rows, 0, 0, false))
assert.Equal(t, ErrColumnNumber, f.adjustFormula("Sheet1", "Sheet1", &xlsxC{F: &xlsxF{Ref: "XFD1:XFD1"}}, columns, 0, 1, false))
_, err := f.adjustFormulaRef("Sheet1", "Sheet1", "XFE1", false, columns, 0, 1)
assert.Equal(t, ErrColumnNumber, err)
_, err = f.adjustFormulaRef("Sheet1", "Sheet1", "XFD1", false, columns, 0, 1)
assert.Equal(t, ErrColumnNumber, err)
f = NewFile()
assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "XFD1"))
assert.Equal(t, ErrColumnNumber, f.InsertCols("Sheet1", "A", 1))
assert.NoError(t, f.SetCellFormula("Sheet1", "B2", fmt.Sprintf("A%d", TotalRows)))
assert.Equal(t, ErrMaxRows, f.InsertRows("Sheet1", 1, 1))
f = NewFile()
assert.NoError(t, f.SetCellFormula("Sheet1", "B3", "SUM(1048576:1:2)"))
assert.Equal(t, ErrMaxRows, f.InsertRows("Sheet1", 1, 1))
f = NewFile()
assert.NoError(t, f.SetCellFormula("Sheet1", "B3", "SUM(XFD:A:B)"))
assert.Equal(t, ErrColumnNumber, f.InsertCols("Sheet1", "A", 1))
f = NewFile()
assert.NoError(t, f.SetCellFormula("Sheet1", "B3", "SUM(A:B:XFD)"))
assert.Equal(t, ErrColumnNumber, f.InsertCols("Sheet1", "A", 1))
// Test adjust formula with defined name in formula text
f = NewFile()
assert.NoError(t, f.SetDefinedName(&DefinedName{
Name: "Amount",
RefersTo: "Sheet1!$B$2",
}))
assert.NoError(t, f.SetCellFormula("Sheet1", "B2", "Amount+B3"))
assert.NoError(t, f.RemoveRow("Sheet1", 1))
formula, err := f.GetCellFormula("Sheet1", "B1")
assert.NoError(t, err)
assert.Equal(t, "Amount+B2", formula)
// Test adjust formula with array formula
f = NewFile()
formulaType, reference := STCellFormulaTypeArray, "A3:A3"
assert.NoError(t, f.SetCellFormula("Sheet1", "A3", "A1:A2", FormulaOpts{Ref: &reference, Type: &formulaType}))
assert.NoError(t, f.InsertRows("Sheet1", 1, 1))
formula, err = f.GetCellFormula("Sheet1", "A4")
assert.NoError(t, err)
assert.Equal(t, "A2:A3", formula)
// Test adjust formula on duplicate row with array formula
f = NewFile()
formulaType, reference = STCellFormulaTypeArray, "A3"
assert.NoError(t, f.SetCellFormula("Sheet1", "A3", "A1:A2", FormulaOpts{Ref: &reference, Type: &formulaType}))
assert.NoError(t, f.InsertRows("Sheet1", 1, 1))
formula, err = f.GetCellFormula("Sheet1", "A4")
assert.NoError(t, err)
assert.Equal(t, "A2:A3", formula)
// Test adjust formula on duplicate row with relative and absolute cell references
f = NewFile()
assert.NoError(t, f.SetCellFormula("Sheet1", "B10", "A$10+$A11&\" \""))
assert.NoError(t, f.DuplicateRowTo("Sheet1", 10, 2))
formula, err = f.GetCellFormula("Sheet1", "B2")
assert.NoError(t, err)
assert.Equal(t, "A$2+$A3&\" \"", formula)
t.Run("for_cells_affected_directly", func(t *testing.T) {
// Test insert row in middle of range with relative and absolute cell references
f := NewFile()
assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "$A1+A$2"))
assert.NoError(t, f.InsertRows("Sheet1", 2, 1))
formula, err := f.GetCellFormula("Sheet1", "B1")
assert.NoError(t, err)
assert.Equal(t, "$A1+A$3", formula)
assert.NoError(t, f.RemoveRow("Sheet1", 2))
formula, err = f.GetCellFormula("Sheet1", "B1")
assert.NoError(t, err)
assert.Equal(t, "$A1+A$2", formula)
// Test insert column in middle of range
f = NewFile()
assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "B1+C1"))
assert.NoError(t, f.InsertCols("Sheet1", "C", 1))
formula, err = f.GetCellFormula("Sheet1", "A1")
assert.NoError(t, err)
assert.Equal(t, "B1+D1", formula)
assert.NoError(t, f.RemoveCol("Sheet1", "C"))
formula, err = f.GetCellFormula("Sheet1", "A1")
assert.NoError(t, err)
assert.Equal(t, "B1+C1", formula)
// Test insert row and column in a rectangular range
f = NewFile()
assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "D4+D5+E4+E5"))
assert.NoError(t, f.InsertCols("Sheet1", "E", 1))
assert.NoError(t, f.InsertRows("Sheet1", 5, 1))
formula, err = f.GetCellFormula("Sheet1", "A1")
assert.NoError(t, err)
assert.Equal(t, "D4+D6+F4+F6", formula)
// Test insert row in middle of range
f = NewFile()
formulaType, reference := STCellFormulaTypeArray, "B1:B1"
assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "A1:A2", FormulaOpts{Ref: &reference, Type: &formulaType}))
assert.NoError(t, f.InsertRows("Sheet1", 2, 1))
formula, err = f.GetCellFormula("Sheet1", "B1")
assert.NoError(t, err)
assert.Equal(t, "A1:A3", formula)
assert.NoError(t, f.RemoveRow("Sheet1", 2))
formula, err = f.GetCellFormula("Sheet1", "B1")
assert.NoError(t, err)
assert.Equal(t, "A1:A2", formula)
// Test insert column in middle of range
f = NewFile()
formulaType, reference = STCellFormulaTypeArray, "A1:A1"
assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "B1:C1", FormulaOpts{Ref: &reference, Type: &formulaType}))
assert.NoError(t, f.InsertCols("Sheet1", "C", 1))
formula, err = f.GetCellFormula("Sheet1", "A1")
assert.NoError(t, err)
assert.Equal(t, "B1:D1", formula)
assert.NoError(t, f.RemoveCol("Sheet1", "C"))
formula, err = f.GetCellFormula("Sheet1", "A1")
assert.NoError(t, err)
assert.Equal(t, "B1:C1", formula)
// Test insert row and column in a rectangular range
f = NewFile()
formulaType, reference = STCellFormulaTypeArray, "A1:A1"
assert.NoError(t, f.SetCellFormula("Sheet1", "A1", "D4:E5", FormulaOpts{Ref: &reference, Type: &formulaType}))
assert.NoError(t, f.InsertCols("Sheet1", "E", 1))
assert.NoError(t, f.InsertRows("Sheet1", 5, 1))
formula, err = f.GetCellFormula("Sheet1", "A1")
assert.NoError(t, err)
assert.Equal(t, "D4:F6", formula)
})
t.Run("for_cells_affected_indirectly", func(t *testing.T) {
f := NewFile()
assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "A3+A4"))
assert.NoError(t, f.InsertRows("Sheet1", 2, 1))
formula, err := f.GetCellFormula("Sheet1", "B1")
assert.NoError(t, err)
assert.Equal(t, "A4+A5", formula)
assert.NoError(t, f.RemoveRow("Sheet1", 2))
formula, err = f.GetCellFormula("Sheet1", "B1")
assert.NoError(t, err)
assert.Equal(t, "A3+A4", formula)
f = NewFile()
assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "D3+D4"))
assert.NoError(t, f.InsertCols("Sheet1", "C", 1))
formula, err = f.GetCellFormula("Sheet1", "B1")
assert.NoError(t, err)
assert.Equal(t, "E3+E4", formula)
assert.NoError(t, f.RemoveCol("Sheet1", "C"))
formula, err = f.GetCellFormula("Sheet1", "B1")
assert.NoError(t, err)
assert.Equal(t, "D3+D4", formula)
})
t.Run("for_entire_cols_rows_reference", func(t *testing.T) {
f := NewFile()
// Test adjust formula on insert row in the middle of the range
assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "SUM(A2:A3:A4,,Table1[])"))
assert.NoError(t, f.InsertRows("Sheet1", 3, 1))
formula, err := f.GetCellFormula("Sheet1", "B1")
assert.NoError(t, err)
assert.Equal(t, "SUM(A2:A4:A5,,Table1[])", formula)
// Test adjust formula on insert at the top of the range
assert.NoError(t, f.InsertRows("Sheet1", 2, 1))
formula, err = f.GetCellFormula("Sheet1", "B1")
assert.NoError(t, err)
assert.Equal(t, "SUM(A3:A5:A6,,Table1[])", formula)
f = NewFile()
// Test adjust formula on insert row in the middle of the range
assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "SUM('Sheet 1'!A2,A3)"))
assert.NoError(t, f.InsertRows("Sheet1", 3, 1))
formula, err = f.GetCellFormula("Sheet1", "B1")
assert.NoError(t, err)
assert.Equal(t, "SUM('Sheet 1'!A2,A4)", formula)
// Test adjust formula on insert row at the top of the range
assert.NoError(t, f.InsertRows("Sheet1", 2, 1))
formula, err = f.GetCellFormula("Sheet1", "B1")
assert.NoError(t, err)
assert.Equal(t, "SUM('Sheet 1'!A2,A5)", formula)
f = NewFile()
// Test adjust formula on insert col in the middle of the range
assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "SUM(C3:D3)"))
assert.NoError(t, f.InsertCols("Sheet1", "D", 1))
formula, err = f.GetCellFormula("Sheet1", "B1")
assert.NoError(t, err)
assert.Equal(t, "SUM(C3:E3)", formula)
// Test adjust formula on insert at the top of the range
assert.NoError(t, f.InsertCols("Sheet1", "C", 1))
formula, err = f.GetCellFormula("Sheet1", "B1")
assert.NoError(t, err)
assert.Equal(t, "SUM(D3:F3)", formula)
f = NewFile()
// Test adjust formula on insert column in the middle of the range
assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "SUM(C3,D3)"))
assert.NoError(t, f.InsertCols("Sheet1", "D", 1))
formula, err = f.GetCellFormula("Sheet1", "B1")
assert.NoError(t, err)
assert.Equal(t, "SUM(C3,E3)", formula)
// Test adjust formula on insert column at the top of the range
assert.NoError(t, f.InsertCols("Sheet1", "C", 1))
formula, err = f.GetCellFormula("Sheet1", "B1")
assert.NoError(t, err)
assert.Equal(t, "SUM(D3,F3)", formula)
f = NewFile()
// Test adjust formula on insert row in the middle of the range (range of whole row)
assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "SUM(2:3)"))
assert.NoError(t, f.InsertRows("Sheet1", 3, 1))
formula, err = f.GetCellFormula("Sheet1", "B1")
assert.NoError(t, err)
assert.Equal(t, "SUM(2:4)", formula)
// Test adjust formula on insert row at the top of the range (range of whole row)
assert.NoError(t, f.InsertRows("Sheet1", 2, 1))
formula, err = f.GetCellFormula("Sheet1", "B1")
assert.NoError(t, err)
assert.Equal(t, "SUM(3:5)", formula)
f = NewFile()
// Test adjust formula on insert row in the middle of the range (range of whole column)
assert.NoError(t, f.SetCellFormula("Sheet1", "B1", "SUM(C:D)"))
assert.NoError(t, f.InsertCols("Sheet1", "D", 1))
formula, err = f.GetCellFormula("Sheet1", "B1")
assert.NoError(t, err)
assert.Equal(t, "SUM(C:E)", formula)
// Test adjust formula on insert row at the top of the range (range of whole column)
assert.NoError(t, f.InsertCols("Sheet1", "C", 1))
formula, err = f.GetCellFormula("Sheet1", "B1")
assert.NoError(t, err)
assert.Equal(t, "SUM(D:F)", formula)
})
t.Run("for_all_worksheet_cells_with_rows_insert", func(t *testing.T) {
f := NewFile()
_, err := f.NewSheet("Sheet2")
assert.NoError(t, err)
// Tests formulas referencing Sheet2 should update but those referencing the original sheet should not
tbl := [][]string{
{"B1", "Sheet2!A1+Sheet2!A2", "Sheet2!A1+Sheet2!A3", "Sheet2!A2+Sheet2!A4"},
{"C1", "A1+A2", "A1+A2", "A1+A2"},
{"D1", "Sheet2!B1:B2", "Sheet2!B1:B3", "Sheet2!B2:B4"},
{"E1", "B1:B2", "B1:B2", "B1:B2"},
{"F1", "SUM(Sheet2!C1:C2)", "SUM(Sheet2!C1:C3)", "SUM(Sheet2!C2:C4)"},
{"G1", "SUM(C1:C2)", "SUM(C1:C2)", "SUM(C1:C2)"},
{"H1", "SUM(Sheet2!D1,Sheet2!D2)", "SUM(Sheet2!D1,Sheet2!D3)", "SUM(Sheet2!D2,Sheet2!D4)"},
{"I1", "SUM(D1,D2)", "SUM(D1,D2)", "SUM(D1,D2)"},
}
for _, preset := range tbl {
assert.NoError(t, f.SetCellFormula("Sheet1", preset[0], preset[1]))
}
// Test adjust formula on insert row in the middle of the range
assert.NoError(t, f.InsertRows("Sheet2", 2, 1))
for _, preset := range tbl {
formula, err := f.GetCellFormula("Sheet1", preset[0])
assert.NoError(t, err)
assert.Equal(t, preset[2], formula)
}
// Test adjust formula on insert row in the top of the range
assert.NoError(t, f.InsertRows("Sheet2", 1, 1))
for _, preset := range tbl {
formula, err := f.GetCellFormula("Sheet1", preset[0])
assert.NoError(t, err)
assert.Equal(t, preset[3], formula)
}
})
t.Run("for_all_worksheet_cells_with_cols_insert", func(t *testing.T) {
f := NewFile()
_, err := f.NewSheet("Sheet2")
assert.NoError(t, err)
tbl := [][]string{
{"A1", "Sheet2!A1+Sheet2!B1", "Sheet2!A1+Sheet2!C1", "Sheet2!B1+Sheet2!D1"},
{"A2", "A1+B1", "A1+B1", "A1+B1"},
{"A3", "Sheet2!A2:B2", "Sheet2!A2:C2", "Sheet2!B2:D2"},
{"A4", "A2:B2", "A2:B2", "A2:B2"},
{"A5", "SUM(Sheet2!A3:B3)", "SUM(Sheet2!A3:C3)", "SUM(Sheet2!B3:D3)"},
{"A6", "SUM(A3:B3)", "SUM(A3:B3)", "SUM(A3:B3)"},
{"A7", "SUM(Sheet2!A4,Sheet2!B4)", "SUM(Sheet2!A4,Sheet2!C4)", "SUM(Sheet2!B4,Sheet2!D4)"},
{"A8", "SUM(A4,B4)", "SUM(A4,B4)", "SUM(A4,B4)"},
}
for _, preset := range tbl {
assert.NoError(t, f.SetCellFormula("Sheet1", preset[0], preset[1]))
}
// Test adjust formula on insert column in the middle of the range
assert.NoError(t, f.InsertCols("Sheet2", "B", 1))
for _, preset := range tbl {
formula, err := f.GetCellFormula("Sheet1", preset[0])
assert.NoError(t, err)
assert.Equal(t, preset[2], formula)
}
// Test adjust formula on insert column in the top of the range
assert.NoError(t, f.InsertCols("Sheet2", "A", 1))
for _, preset := range tbl {
formula, err := f.GetCellFormula("Sheet1", preset[0])
assert.NoError(t, err)
assert.Equal(t, preset[3], formula)
}
})
t.Run("for_cross_sheet_ref_with_rows_insert)", func(t *testing.T) {
f := NewFile()
_, err := f.NewSheet("Sheet2")
assert.NoError(t, err)
_, err = f.NewSheet("Sheet3")
assert.NoError(t, err)
// Tests formulas referencing Sheet2 should update but those referencing
// the original sheet or Sheet 3 should not update
tbl := [][]string{
{"B1", "Sheet2!A1+Sheet2!A2+Sheet1!A3+Sheet1!A4", "Sheet2!A1+Sheet2!A3+Sheet1!A3+Sheet1!A4", "Sheet2!A2+Sheet2!A4+Sheet1!A3+Sheet1!A4"},
{"C1", "Sheet2!B1+Sheet2!B2+B3+B4", "Sheet2!B1+Sheet2!B3+B3+B4", "Sheet2!B2+Sheet2!B4+B3+B4"},
{"D1", "Sheet2!C1+Sheet2!C2+Sheet3!A3+Sheet3!A4", "Sheet2!C1+Sheet2!C3+Sheet3!A3+Sheet3!A4", "Sheet2!C2+Sheet2!C4+Sheet3!A3+Sheet3!A4"},
{"E1", "SUM(Sheet2!D1:D2,Sheet1!A3:A4)", "SUM(Sheet2!D1:D3,Sheet1!A3:A4)", "SUM(Sheet2!D2:D4,Sheet1!A3:A4)"},
{"F1", "SUM(Sheet2!E1:E2,A3:A4)", "SUM(Sheet2!E1:E3,A3:A4)", "SUM(Sheet2!E2:E4,A3:A4)"},
{"G1", "SUM(Sheet2!F1:F2,Sheet3!A3:A4)", "SUM(Sheet2!F1:F3,Sheet3!A3:A4)", "SUM(Sheet2!F2:F4,Sheet3!A3:A4)"},
}
for _, preset := range tbl {
assert.NoError(t, f.SetCellFormula("Sheet1", preset[0], preset[1]))
}
// Test adjust formula on insert row in the middle of the range
assert.NoError(t, f.InsertRows("Sheet2", 2, 1))
for _, preset := range tbl {
formula, err := f.GetCellFormula("Sheet1", preset[0])
assert.NoError(t, err)
assert.Equal(t, preset[2], formula)
}
// Test adjust formula on insert row in the top of the range
assert.NoError(t, f.InsertRows("Sheet2", 1, 1))
for _, preset := range tbl {
formula, err := f.GetCellFormula("Sheet1", preset[0])
assert.NoError(t, err)
assert.Equal(t, preset[3], formula)
}
})
t.Run("for_cross_sheet_ref_with_cols_insert)", func(t *testing.T) {
f := NewFile()
_, err := f.NewSheet("Sheet2")
assert.NoError(t, err)
_, err = f.NewSheet("Sheet3")
assert.NoError(t, err)
// Tests formulas referencing Sheet2 should update but those referencing
// the original sheet or Sheet 3 should not update
tbl := [][]string{
{"A1", "Sheet2!A1+Sheet2!B1+Sheet1!C1+Sheet1!D1", "Sheet2!A1+Sheet2!C1+Sheet1!C1+Sheet1!D1", "Sheet2!B1+Sheet2!D1+Sheet1!C1+Sheet1!D1"},
{"A2", "Sheet2!A2+Sheet2!B2+C2+D2", "Sheet2!A2+Sheet2!C2+C2+D2", "Sheet2!B2+Sheet2!D2+C2+D2"},
{"A3", "Sheet2!A3+Sheet2!B3+Sheet3!C3+Sheet3!D3", "Sheet2!A3+Sheet2!C3+Sheet3!C3+Sheet3!D3", "Sheet2!B3+Sheet2!D3+Sheet3!C3+Sheet3!D3"},
{"A4", "SUM(Sheet2!A4:B4,Sheet1!C4:D4)", "SUM(Sheet2!A4:C4,Sheet1!C4:D4)", "SUM(Sheet2!B4:D4,Sheet1!C4:D4)"},
{"A5", "SUM(Sheet2!A5:B5,C5:D5)", "SUM(Sheet2!A5:C5,C5:D5)", "SUM(Sheet2!B5:D5,C5:D5)"},
{"A6", "SUM(Sheet2!A6:B6,Sheet3!C6:D6)", "SUM(Sheet2!A6:C6,Sheet3!C6:D6)", "SUM(Sheet2!B6:D6,Sheet3!C6:D6)"},
}
for _, preset := range tbl {
assert.NoError(t, f.SetCellFormula("Sheet1", preset[0], preset[1]))
}
// Test adjust formula on insert row in the middle of the range
assert.NoError(t, f.InsertCols("Sheet2", "B", 1))
for _, preset := range tbl {
formula, err := f.GetCellFormula("Sheet1", preset[0])
assert.NoError(t, err)
assert.Equal(t, preset[2], formula)
}
// Test adjust formula on insert row in the top of the range
assert.NoError(t, f.InsertCols("Sheet2", "A", 1))
for _, preset := range tbl {
formula, err := f.GetCellFormula("Sheet1", preset[0])
assert.NoError(t, err)
assert.Equal(t, preset[3], formula)
}
})
t.Run("for_cross_sheet_ref_with_chart_sheet)", func(t *testing.T) {
assert.NoError(t, f.AddChartSheet("Chart1", &Chart{Type: Line}))
assert.NoError(t, f.InsertRows("Sheet1", 2, 1))
assert.NoError(t, f.InsertCols("Sheet1", "A", 1))
})
t.Run("for_array_formula_cell", func(t *testing.T) {
f := NewFile()
assert.NoError(t, f.SetSheetRow("Sheet1", "A1", &[]int{1, 2}))
assert.NoError(t, f.SetSheetRow("Sheet1", "A2", &[]int{3, 4}))
formulaType, ref := STCellFormulaTypeArray, "C1:C2"
assert.NoError(t, f.SetCellFormula("Sheet1", "C1", "A1:A2*B1:B2", FormulaOpts{Ref: &ref, Type: &formulaType}))
assert.NoError(t, f.InsertRows("Sheet1", 1, 1))
assert.NoError(t, f.InsertCols("Sheet1", "A", 1))
result, err := f.CalcCellValue("Sheet1", "D2")
assert.NoError(t, err)
assert.Equal(t, "2", result)
result, err = f.CalcCellValue("Sheet1", "D3")
assert.NoError(t, err)
assert.Equal(t, "12", result)
// Test adjust array formula with invalid range reference
formulaType, ref = STCellFormulaTypeArray, "E1:E2"
assert.NoError(t, f.SetCellFormula("Sheet1", "E1", "XFD1:XFD1", FormulaOpts{Ref: &ref, Type: &formulaType}))
assert.EqualError(t, f.InsertCols("Sheet1", "A", 1), "the column number must be greater than or equal to 1 and less than or equal to 16384")
})
}
func TestAdjustVolatileDeps(t *testing.T) {
f := NewFile()
f.Pkg.Store(defaultXMLPathVolatileDeps, []byte(fmt.Sprintf(`<volTypes xmlns="%s"><volType><main><tp><tr r="C2" s="2"/><tr r="C2" s="1"/><tr r="D3" s="1"/></tp></main></volType></volTypes>`, NameSpaceSpreadSheet.Value)))
assert.NoError(t, f.InsertCols("Sheet1", "A", 1))
assert.NoError(t, f.InsertRows("Sheet1", 2, 1))
assert.Equal(t, "D3", f.VolatileDeps.VolType[0].Main[0].Tp[0].Tr[1].R)
assert.NoError(t, f.RemoveCol("Sheet1", "D"))
assert.NoError(t, f.RemoveRow("Sheet1", 4))
assert.Len(t, f.VolatileDeps.VolType[0].Main[0].Tp[0].Tr, 1)
f = NewFile()
f.Pkg.Store(defaultXMLPathVolatileDeps, MacintoshCyrillicCharset)
assert.EqualError(t, f.InsertRows("Sheet1", 2, 1), "XML syntax error on line 1: invalid UTF-8")
f = NewFile()
f.Pkg.Store(defaultXMLPathVolatileDeps, []byte(fmt.Sprintf(`<volTypes xmlns="%s"><volType><main><tp><tr r="A" s="1"/></tp></main></volType></volTypes>`, NameSpaceSpreadSheet.Value)))
assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.InsertCols("Sheet1", "A", 1))
f.volatileDepsWriter()
}
func TestAdjustConditionalFormats(t *testing.T) {
f := NewFile()
assert.NoError(t, f.SetSheetRow("Sheet1", "B1", &[]interface{}{1, nil, 1, 1}))
formatID, err := f.NewConditionalStyle(&Style{Font: &Font{Color: "09600B"}, Fill: Fill{Type: "pattern", Color: []string{"C7EECF"}, Pattern: 1}})
assert.NoError(t, err)
format := []ConditionalFormatOptions{
{
Type: "cell",
Criteria: "greater than",
Format: &formatID,
Value: "0",
},
}
for _, ref := range []string{"B1", "D1:E1"} {
assert.NoError(t, f.SetConditionalFormat("Sheet1", ref, format))
}
assert.NoError(t, f.RemoveCol("Sheet1", "B"))
opts, err := f.GetConditionalFormats("Sheet1")
assert.NoError(t, err)
assert.Len(t, format, 1)
assert.Equal(t, format, opts["C1:D1"])
ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
assert.True(t, ok)
ws.(*xlsxWorksheet).ConditionalFormatting[0].SQRef = "-"
assert.Equal(t, newCellNameToCoordinatesError("-", newInvalidCellNameError("-")), f.RemoveCol("Sheet1", "B"))
ws.(*xlsxWorksheet).ConditionalFormatting[0] = nil
assert.NoError(t, f.RemoveCol("Sheet1", "B"))
t.Run("for_remove_conditional_formats_column", func(t *testing.T) {
f := NewFile()
format := []ConditionalFormatOptions{{
Type: "data_bar",
Criteria: "=",
MinType: "min",
MaxType: "max",
BarColor: "#638EC6",
}}
assert.NoError(t, f.SetConditionalFormat("Sheet1", "D2:D3", format))
assert.NoError(t, f.SetConditionalFormat("Sheet1", "D5", format))
assert.NoError(t, f.RemoveCol("Sheet1", "D"))
opts, err := f.GetConditionalFormats("Sheet1")
assert.NoError(t, err)
assert.Len(t, opts, 0)
})
t.Run("for_remove_conditional_formats_row", func(t *testing.T) {
f := NewFile()
format := []ConditionalFormatOptions{{
Type: "data_bar",
Criteria: "=",
MinType: "min",
MaxType: "max",
BarColor: "#638EC6",
}}
assert.NoError(t, f.SetConditionalFormat("Sheet1", "D2:E2", format))
assert.NoError(t, f.SetConditionalFormat("Sheet1", "F2", format))
assert.NoError(t, f.RemoveRow("Sheet1", 2))
opts, err := f.GetConditionalFormats("Sheet1")
assert.NoError(t, err)
assert.Len(t, opts, 0)
})
t.Run("for_adjust_conditional_formats_row", func(t *testing.T) {
f := NewFile()
format := []ConditionalFormatOptions{{
Type: "data_bar",
Criteria: "=",
MinType: "min",
MaxType: "max",
BarColor: "#638EC6",
}}
assert.NoError(t, f.SetConditionalFormat("Sheet1", "D2:D3", format))
assert.NoError(t, f.SetConditionalFormat("Sheet1", "D5", format))
assert.NoError(t, f.RemoveRow("Sheet1", 1))
opts, err := f.GetConditionalFormats("Sheet1")
assert.NoError(t, err)
assert.Len(t, opts, 2)
assert.Equal(t, format, opts["D1:D2"])
assert.Equal(t, format, opts["D4:D4"])
})
}
func TestAdjustDataValidations(t *testing.T) {
f := NewFile()
dv := NewDataValidation(true)
dv.Sqref = "B1"
assert.NoError(t, dv.SetDropList([]string{"1", "2", "3"}))
assert.NoError(t, f.AddDataValidation("Sheet1", dv))
assert.NoError(t, f.RemoveCol("Sheet1", "B"))
dvs, err := f.GetDataValidations("Sheet1")
assert.NoError(t, err)
assert.Len(t, dvs, 0)
assert.NoError(t, f.SetCellValue("Sheet1", "F2", 1))
assert.NoError(t, f.SetCellValue("Sheet1", "F3", 2))
dv = NewDataValidation(true)
dv.Sqref = "C2:D3"
dv.SetSqrefDropList("$F$2:$F$3")
assert.NoError(t, f.AddDataValidation("Sheet1", dv))
assert.NoError(t, f.AddChartSheet("Chart1", &Chart{Type: Line}))
_, err = f.NewSheet("Sheet2")
assert.NoError(t, err)
assert.NoError(t, f.SetSheetRow("Sheet2", "C1", &[]interface{}{1, 10}))
dv = NewDataValidation(true)
dv.Sqref = "C5:D6"
assert.NoError(t, dv.SetRange("Sheet2!C1", "Sheet2!D1", DataValidationTypeWhole, DataValidationOperatorBetween))
dv.SetError(DataValidationErrorStyleStop, "error title", "error body")
assert.NoError(t, f.AddDataValidation("Sheet1", dv))
assert.NoError(t, f.RemoveCol("Sheet1", "B"))
assert.NoError(t, f.RemoveCol("Sheet2", "B"))
dvs, err = f.GetDataValidations("Sheet1")
assert.NoError(t, err)
assert.Equal(t, "B2:C3", dvs[0].Sqref)
assert.Equal(t, "$E$2:$E$3", dvs[0].Formula1)
assert.Equal(t, "B5:C6", dvs[1].Sqref)
assert.Equal(t, "Sheet2!B1", dvs[1].Formula1)
assert.Equal(t, "Sheet2!C1", dvs[1].Formula2)
dv = NewDataValidation(true)
dv.Sqref = "C8:D10"
assert.NoError(t, dv.SetDropList([]string{`A<`, `B>`, `C"`, "D\t", `E'`, `F`}))
assert.NoError(t, f.AddDataValidation("Sheet1", dv))
assert.NoError(t, f.RemoveCol("Sheet1", "B"))
dvs, err = f.GetDataValidations("Sheet1")
assert.NoError(t, err)
assert.Equal(t, "\"A<,B>,C\",D\t,E',F\"", dvs[2].Formula1)
// Test adjust data validation with multiple cell range
dv = NewDataValidation(true)
dv.Sqref = "G1:G3 H1:H3 A3:A1048576"
assert.NoError(t, dv.SetDropList([]string{"1", "2", "3"}))
assert.NoError(t, f.AddDataValidation("Sheet1", dv))
assert.NoError(t, f.InsertRows("Sheet1", 2, 1))
dvs, err = f.GetDataValidations("Sheet1")
assert.NoError(t, err)
assert.Equal(t, "G1:G4 H1:H4 A4:A1048576", dvs[3].Sqref)
dv = NewDataValidation(true)
dv.Sqref = "C5:D6"
assert.NoError(t, dv.SetRange("Sheet1!A1048576", "Sheet1!XFD1", DataValidationTypeWhole, DataValidationOperatorBetween))
dv.SetError(DataValidationErrorStyleStop, "error title", "error body")
assert.NoError(t, f.AddDataValidation("Sheet1", dv))
assert.Equal(t, ErrColumnNumber, f.InsertCols("Sheet1", "A", 1))
assert.Equal(t, ErrMaxRows, f.InsertRows("Sheet1", 1, 1))
ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
assert.True(t, ok)
ws.(*xlsxWorksheet).DataValidations.DataValidation[0].Sqref = "-"
assert.Equal(t, newCellNameToCoordinatesError("-", newInvalidCellNameError("-")), f.RemoveCol("Sheet1", "B"))
ws.(*xlsxWorksheet).DataValidations.DataValidation[0] = nil
assert.NoError(t, f.RemoveCol("Sheet1", "B"))
ws.(*xlsxWorksheet).DataValidations = nil
assert.NoError(t, f.RemoveCol("Sheet1", "B"))
f.Sheet.Delete("xl/worksheets/sheet1.xml")
f.Pkg.Store("xl/worksheets/sheet1.xml", MacintoshCyrillicCharset)
assert.EqualError(t, f.adjustDataValidations(nil, "Sheet1", columns, 0, 0, 1), "XML syntax error on line 1: invalid UTF-8")
t.Run("for_escaped_data_validation_rules_formula", func(t *testing.T) {
f := NewFile()
_, err := f.NewSheet("Sheet2")
assert.NoError(t, err)
dv := NewDataValidation(true)
dv.Sqref = "A1"
assert.NoError(t, dv.SetDropList([]string{"option1", strings.Repeat("\"", 4)}))
ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
assert.True(t, ok)
assert.NoError(t, f.AddDataValidation("Sheet1", dv))
// The double quote symbol in none formula data validation rules will be escaped in the Kingsoft WPS Office
formula := strings.ReplaceAll(fmt.Sprintf("\"option1, %s", strings.Repeat("\"", 9)), "\"", "&quot;")
ws.(*xlsxWorksheet).DataValidations.DataValidation[0].Formula1.Content = formula
assert.NoError(t, f.RemoveCol("Sheet2", "A"))
dvs, err := f.GetDataValidations("Sheet1")
assert.NoError(t, err)
assert.Equal(t, formula, dvs[0].Formula1)
})
}
func TestAdjustDrawings(t *testing.T) {
f := NewFile()
// Test add pictures to sheet with positioning
assert.NoError(t, f.AddPicture("Sheet1", "B2", filepath.Join("test", "images", "excel.jpg"), nil))
assert.NoError(t, f.AddPicture("Sheet1", "B11", filepath.Join("test", "images", "excel.jpg"), &GraphicOptions{Positioning: "oneCell"}))
assert.NoError(t, f.AddPicture("Sheet1", "B21", filepath.Join("test", "images", "excel.jpg"), &GraphicOptions{Positioning: "absolute"}))
// Test adjust pictures on inserting columns and rows
assert.NoError(t, f.InsertCols("Sheet1", "A", 1))
assert.NoError(t, f.InsertRows("Sheet1", 1, 1))
assert.NoError(t, f.InsertCols("Sheet1", "C", 1))
assert.NoError(t, f.InsertRows("Sheet1", 5, 1))
assert.NoError(t, f.InsertRows("Sheet1", 15, 1))
cells, err := f.GetPictureCells("Sheet1")
assert.NoError(t, err)
assert.Equal(t, []string{"D3", "D13", "B21"}, cells)
wb := filepath.Join("test", "TestAdjustDrawings.xlsx")
assert.NoError(t, f.SaveAs(wb))
// Test adjust pictures on deleting columns and rows
assert.NoError(t, f.RemoveCol("Sheet1", "A"))
assert.NoError(t, f.RemoveRow("Sheet1", 1))
cells, err = f.GetPictureCells("Sheet1")
assert.NoError(t, err)
assert.Equal(t, []string{"C2", "C12", "B21"}, cells)
// Test adjust existing pictures on inserting columns and rows
f, err = OpenFile(wb)
assert.NoError(t, err)
assert.NoError(t, f.InsertCols("Sheet1", "A", 1))
assert.NoError(t, f.InsertRows("Sheet1", 1, 1))
assert.NoError(t, f.InsertCols("Sheet1", "D", 1))
assert.NoError(t, f.InsertRows("Sheet1", 5, 1))
assert.NoError(t, f.InsertRows("Sheet1", 16, 1))
cells, err = f.GetPictureCells("Sheet1")
assert.NoError(t, err)
assert.Equal(t, []string{"F4", "F15", "B21"}, cells)
// Test adjust drawings with unsupported charset
f, err = OpenFile(wb)
assert.NoError(t, err)
f.Pkg.Store("xl/drawings/drawing1.xml", MacintoshCyrillicCharset)
assert.EqualError(t, f.InsertCols("Sheet1", "A", 1), "XML syntax error on line 1: invalid UTF-8")
errors := []error{ErrColumnNumber, ErrColumnNumber, ErrMaxRows, ErrMaxRows}
cells = []string{"XFD1", "XFB1"}
for i, cell := range cells {
f = NewFile()
assert.NoError(t, f.AddPicture("Sheet1", cell, filepath.Join("test", "images", "excel.jpg"), nil))
assert.Equal(t, errors[i], f.InsertCols("Sheet1", "A", 1))
assert.NoError(t, f.SaveAs(wb))
f, err = OpenFile(wb)
assert.NoError(t, err)
assert.Equal(t, errors[i], f.InsertCols("Sheet1", "A", 1))
}
errors = []error{ErrMaxRows, ErrMaxRows}
cells = []string{"A1048576", "A1048570"}
for i, cell := range cells {
f = NewFile()
assert.NoError(t, f.AddPicture("Sheet1", cell, filepath.Join("test", "images", "excel.jpg"), nil))
assert.Equal(t, errors[i], f.InsertRows("Sheet1", 1, 1))
assert.NoError(t, f.SaveAs(wb))
f, err = OpenFile(wb)
assert.NoError(t, err)
assert.Equal(t, errors[i], f.InsertRows("Sheet1", 1, 1))
}
a := xdrCellAnchor{}
assert.NoError(t, a.adjustDrawings(columns, 0, 0))
p := xlsxCellAnchorPos{}
assert.NoError(t, p.adjustDrawings(columns, 0, 0, ""))
f, err = OpenFile(wb)
assert.NoError(t, err)
f.Pkg.Store("xl/drawings/drawing1.xml", []byte(xml.Header+`<wsDr xmlns="http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing"><twoCellAnchor><from><col>0</col><colOff>0</colOff><row>0</row><rowOff>0</rowOff></from><to><col>1</col><colOff>0</colOff><row>1</row><rowOff>0</rowOff></to><mc:AlternateContent xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"></mc:AlternateContent><clientData/></twoCellAnchor></wsDr>`))
assert.NoError(t, f.InsertCols("Sheet1", "A", 1))
}
func TestAdjustDefinedNames(t *testing.T) {
f := NewFile()
_, err := f.NewSheet("Sheet2")
assert.NoError(t, err)
for _, dn := range []*DefinedName{
{Name: "Name1", RefersTo: "Sheet1!$XFD$1"},
{Name: "Name2", RefersTo: "Sheet2!$C$1", Scope: "Sheet1"},
{Name: "Name3", RefersTo: "Sheet2!$C$1:$D$2", Scope: "Sheet1"},
{Name: "Name4", RefersTo: "Sheet2!$C1:D$2"},
{Name: "Name5", RefersTo: "Sheet2!C$1:$D2"},
{Name: "Name6", RefersTo: "Sheet2!C:$D"},
{Name: "Name7", RefersTo: "Sheet2!$C:D"},
{Name: "Name8", RefersTo: "Sheet2!C:D"},
{Name: "Name9", RefersTo: "Sheet2!$C:$D"},
{Name: "Name10", RefersTo: "Sheet2!1:2"},
} {
assert.NoError(t, f.SetDefinedName(dn))
}
assert.NoError(t, f.InsertCols("Sheet1", "A", 1))
assert.NoError(t, f.InsertRows("Sheet1", 1, 1))
assert.NoError(t, f.InsertCols("Sheet2", "A", 1))
assert.NoError(t, f.InsertRows("Sheet2", 1, 1))
definedNames := f.GetDefinedName()
for i, expected := range []string{
"Sheet1!$XFD$2",
"Sheet2!$D$2",
"Sheet2!$D$2:$E$3",
"Sheet2!$D1:D$3",
"Sheet2!C$2:$E2",
"Sheet2!C:$E",
"Sheet2!$D:D",
"Sheet2!C:D",
"Sheet2!$D:$E",
"Sheet2!1:2",
} {
assert.Equal(t, expected, definedNames[i].RefersTo)
}
f = NewFile()
assert.NoError(t, f.SetDefinedName(&DefinedName{
Name: "Name1",
RefersTo: "Sheet1!$A$1",
Scope: "Sheet1",
}))
assert.NoError(t, f.RemoveCol("Sheet1", "A"))
definedNames = f.GetDefinedName()
assert.Equal(t, "Sheet1!$A$1", definedNames[0].RefersTo)
f = NewFile()
assert.NoError(t, f.SetDefinedName(&DefinedName{
Name: "Name1",
RefersTo: "'1.A & B C'!#REF!",
Scope: "Sheet1",
}))
assert.NoError(t, f.RemoveCol("Sheet1", "A"))
definedNames = f.GetDefinedName()
assert.Equal(t, "'1.A & B C'!#REF!", definedNames[0].RefersTo)
f = NewFile()
f.WorkBook = nil
f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset)
assert.EqualError(t, f.adjustDefinedNames(nil, "Sheet1", columns, 0, 0, 1), "XML syntax error on line 1: invalid UTF-8")
}

1620
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
// the LICENSE file.
//
@ -7,7 +7,7 @@
// 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.
// data. This library needs Go version 1.18 or later.
package excelize
@ -81,3 +81,39 @@ func (c xlsxCalcChainCollection) Filter(fn func(v xlsxCalcChainC) bool) []xlsxCa
}
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:]...)
}
}
}

257
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
// the LICENSE file.
//
@ -7,7 +7,7 @@
// 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.
// data. This library needs Go version 1.18 or later.
package excelize
@ -15,6 +15,7 @@ import (
"bytes"
"encoding/xml"
"fmt"
"math"
"os"
"reflect"
"strconv"
@ -72,7 +73,7 @@ func (f *File) GetCellValue(sheet, cell string, opts ...Options) (string, error)
if err != nil {
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
})
}
@ -143,7 +144,7 @@ func (f *File) SetCellValue(sheet, cell string, value interface{}) error {
if err != nil {
return err
}
err = f.setDefaultTimeStyle(sheet, cell, 21)
err = f.setDefaultTimeStyle(sheet, cell, getDurationNumFmt(v))
case time.Time:
err = f.setCellTimeFunc(sheet, cell, v)
case bool:
@ -216,15 +217,15 @@ func (f *File) setCellIntFunc(sheet, cell string, value interface{}) error {
case int64:
err = f.SetCellInt(sheet, cell, int(v))
case uint:
err = f.SetCellInt(sheet, cell, int(v))
err = f.SetCellUint(sheet, cell, uint64(v))
case uint8:
err = f.SetCellInt(sheet, cell, int(v))
err = f.SetCellUint(sheet, cell, uint64(v))
case uint16:
err = f.SetCellInt(sheet, cell, int(v))
err = f.SetCellUint(sheet, cell, uint64(v))
case uint32:
err = f.SetCellInt(sheet, cell, int(v))
err = f.SetCellUint(sheet, cell, uint64(v))
case uint64:
err = f.SetCellInt(sheet, cell, int(v))
err = f.SetCellUint(sheet, cell, v)
}
return err
}
@ -255,7 +256,7 @@ func (f *File) setCellTimeFunc(sheet, cell string, value time.Time) error {
return err
}
if isNum {
_ = f.setDefaultTimeStyle(sheet, cell, 22)
_ = f.setDefaultTimeStyle(sheet, cell, getTimeNumFmt(value))
}
return err
}
@ -307,13 +308,41 @@ func (f *File) SetCellInt(sheet, cell string, value int) error {
return f.removeFormula(c, ws, sheet)
}
// setCellInt prepares cell type and string type cell value by a given
// integer.
// setCellInt prepares cell type and string type cell value by a given integer.
func setCellInt(value int) (t string, v string) {
v = strconv.Itoa(value)
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
// worksheet name, cell reference and cell value.
func (f *File) SetCellBool(sheet, cell string, value bool) error {
@ -336,8 +365,8 @@ func (f *File) SetCellBool(sheet, cell string, value bool) error {
return f.removeFormula(c, ws, sheet)
}
// setCellBool prepares cell type and string type cell value by a given
// boolean value.
// setCellBool prepares cell type and string type cell value by a given boolean
// value.
func setCellBool(value bool) (t string, v string) {
t = "b"
if value {
@ -357,6 +386,9 @@ func setCellBool(value bool) (t string, v string) {
// var x float32 = 1.325
// f.SetCellFloat("Sheet1", "A1", float64(x), 2, 32)
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)
if err != nil {
@ -371,17 +403,20 @@ func (f *File) SetCellFloat(sheet, cell string, value float64, precision, bitSiz
return err
}
c.S = ws.prepareCellStyle(col, row, c.S)
c.T, c.V = setCellFloat(value, precision, bitSize)
c.IS = nil
c.setCellFloat(value, precision, bitSize)
return f.removeFormula(c, ws, sheet)
}
// setCellFloat prepares cell type and string type cell value by a given
// float value.
func setCellFloat(value float64, precision, bitSize int) (t string, v string) {
v = strconv.FormatFloat(value, 'f', precision, bitSize)
// setCellFloat prepares cell type and string type cell value by a given float
// value.
func (c *xlsxC) setCellFloat(value float64, precision, bitSize int) {
if math.IsNaN(value) || math.IsInf(value, 0) {
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
// number of characters that a cell can contain 32767 characters.
@ -407,8 +442,7 @@ func (f *File) SetCellStr(sheet, cell, value string) error {
return f.removeFormula(c, ws, sheet)
}
// setCellString provides a function to set string type to shared string
// table.
// setCellString provides a function to set string type to shared string table.
func (f *File) setCellString(value string) (t, v string, err error) {
if utf8.RuneCountInString(value) > TotalCellChars {
value = string([]rune(value)[:TotalCellChars])
@ -462,11 +496,11 @@ func (f *File) setSharedString(val string) (int, error) {
}
sst.mu.Lock()
defer sst.mu.Unlock()
sst.Count++
sst.UniqueCount++
t := xlsxT{Val: val}
val, t.Space = trimCellValue(val, false)
sst.SI = append(sst.SI, xlsxSI{T: &t})
sst.Count = len(sst.SI)
sst.UniqueCount = sst.Count
f.sharedStringsMap[val] = sst.UniqueCount - 1
return sst.UniqueCount - 1, nil
}
@ -478,7 +512,9 @@ func trimCellValue(value string, escape bool) (v string, ns xml.Attr) {
}
if escape {
var buf bytes.Buffer
_ = xml.EscapeText(&buf, []byte(value))
enc := xml.NewEncoder(&buf)
_ = enc.EncodeToken(xml.CharData(value))
enc.Flush()
value = buf.String()
}
if len(value) > 0 {
@ -520,7 +556,7 @@ func (c *xlsxC) setStr(val string) {
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) {
if !raw {
if c.V == "1" {
@ -565,7 +601,7 @@ func (c *xlsxC) getCellDate(f *File, raw bool) (string, error) {
c.V = strconv.FormatFloat(excelTime, 'G', 15, 64)
}
}
return f.formattedValue(c, raw, CellTypeBool)
return f.formattedValue(c, raw, CellTypeDate)
}
// getValueFrom return a value from a column/row cell, this function is
@ -590,6 +626,8 @@ func (c *xlsxC) getValueFrom(f *File, d *xlsxSST, raw bool) (string, error) {
}
}
return f.formattedValue(c, raw, CellTypeSharedString)
case "str":
return c.V, nil
case "inlineStr":
if c.IS != nil {
return f.formattedValue(&xlsxC{S: c.S, V: c.IS.String()}, raw, CellTypeInlineString)
@ -631,7 +669,22 @@ func (f *File) SetCellDefault(sheet, cell, value string) error {
// GetCellFormula provides a function to get formula from cell by given
// worksheet name and cell reference in spreadsheet.
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) {
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 {
return "", false, nil
}
@ -756,6 +809,11 @@ func (f *File) SetCellFormula(sheet, cell, formula string, opts ...FormulaOpts)
return err
}
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 err = ws.setSharedFormula(*opt.Ref); err != nil {
return err
@ -770,6 +828,67 @@ func (f *File) SetCellFormula(sheet, cell, formula string, opts ...FormulaOpts)
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.
func (ws *xlsxWorksheet) setSharedFormula(ref string) error {
coordinates, err := rangeRefToCoordinates(ref)
@ -845,14 +964,36 @@ type HyperlinkOpts struct {
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
// 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
// this workbook. Maximum limit hyperlinks in a worksheet is 65530. This
// function is only used to set the hyperlink of the cell and doesn't affect
// the value of the cell. If you need to set the value of the cell, please use
// the other functions such as `SetCellStyle` or `SetSheetRow`. The below is
// example for external link.
// this workbook or "None" for remove hyperlink. Maximum limit hyperlinks in a
// worksheet is 65530. This function is only used to set the hyperlink of the
// cell and doesn't affect the value of the cell. If you need to set the value
// of the cell, please use the other functions such as `SetCellStyle` or
// `SetSheetRow`. The below is example for external link.
//
// display, tooltip := "https://github.com/xuri/excelize", "Excelize on GitHub"
// if err := f.SetCellHyperLink("Sheet1", "A3",
@ -920,8 +1061,10 @@ func (f *File) SetCellHyperLink(sheet, cell, link, linkType string, opts ...Hype
Ref: cell,
Location: link,
}
case "None":
return f.removeHyperLink(ws, sheet, cell)
default:
return fmt.Errorf("invalid link type %q", linkType)
return newInvalidLinkTypeError(linkType)
}
for _, o := range opts {
@ -942,6 +1085,9 @@ func (f *File) SetCellHyperLink(sheet, cell, link, linkType string, opts ...Hype
// getCellRichText returns rich text of cell by given string item.
func getCellRichText(si *xlsxSI) (runs []RichTextRun) {
if si.T != nil {
runs = append(runs, RichTextRun{Text: si.T.Val})
}
for _, v := range si.R {
run := RichTextRun{
Text: v.T.Val,
@ -965,8 +1111,15 @@ func (f *File) GetCellRichText(sheet, cell string) (runs []RichTextRun, err erro
if err != nil {
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)
if err != nil || c.T != "s" {
if err != nil {
return
}
sst, err := f.sharedStringsReader()
@ -1288,10 +1441,15 @@ func (ws *xlsxWorksheet) prepareCell(cell string) (*xlsxC, int, int, error) {
// value function. Passed function implements specific part of required
// logic.
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)
if err != nil {
f.mu.Unlock()
return "", err
}
f.mu.Unlock()
ws.mu.Lock()
defer ws.mu.Unlock()
cell, err = ws.mergeCellsParser(cell)
if err != nil {
return "", err
@ -1300,10 +1458,6 @@ func (f *File) getCellStringFunc(sheet, cell string, fn func(x *xlsxWorksheet, c
if err != nil {
return "", err
}
ws.mu.Lock()
defer ws.mu.Unlock()
lastRowNum := 0
if l := len(ws.SheetData.Row); l > 0 {
lastRowNum = ws.SheetData.Row[l-1].R
@ -1365,18 +1519,29 @@ func (f *File) formattedValue(c *xlsxC, raw bool, cellType CellType) (string, er
if wb != nil && wb.WorkbookPr != nil {
date1904 = wb.WorkbookPr.Date1904
}
if fmtCode, ok := styleSheet.getCustomNumFmtCode(numFmtID); ok {
return format(c.V, fmtCode, date1904, cellType, f.options), err
}
if fmtCode, ok := f.getBuiltInNumFmtCode(numFmtID); ok {
return f.applyBuiltInNumFmt(c, fmtCode, numFmtID, date1904, cellType), err
}
if styleSheet.NumFmts == nil {
return c.V, err
}
for _, xlsxFmt := range styleSheet.NumFmts.NumFmt {
// 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 {
return format(c.V, xlsxFmt.FormatCode, date1904, cellType, f.options), err
if xlsxFmt.FormatCode16 != "" {
return xlsxFmt.FormatCode16, true
}
return xlsxFmt.FormatCode, true
}
}
return c.V, err
return "", false
}
// prepareCellStyle provides a function to prepare style index of cell in
@ -1523,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
// allow range reference, and always return origin shared formula.
func getSharedFormula(ws *xlsxWorksheet, si int, cell string) string {
for _, r := range ws.SheetData.Row {
for _, c := range r.C {
for row := 0; row < len(ws.SheetData.Row); row++ {
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 {
col, row, _ := CellNameToCoordinates(cell)
sharedCol, sharedRow, _ := CellNameToCoordinates(c.R)

View File

@ -3,6 +3,7 @@ package excelize
import (
"fmt"
_ "image/jpeg"
"math"
"os"
"path/filepath"
"reflect"
@ -41,6 +42,9 @@ func TestConcurrency(t *testing.T) {
assert.NoError(t, err)
// Concurrency set cell 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
assert.NoError(t, f.AddPicture("Sheet1", "F21", filepath.Join("test", "images", "excel.jpg"),
&GraphicOptions{
@ -87,6 +91,14 @@ func TestConcurrency(t *testing.T) {
visible, err := f.GetColVisible("Sheet1", "A")
assert.NoError(t, err)
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()
}(i, t)
}
@ -96,6 +108,10 @@ func TestConcurrency(t *testing.T) {
t.Error(err)
}
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.Close())
}
@ -133,11 +149,11 @@ func TestCheckCellInRangeRef(t *testing.T) {
}
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)
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)
}
@ -171,9 +187,29 @@ func TestSetCellFloat(t *testing.T) {
assert.Equal(t, "123.42", val, "A1 should be 123.42")
})
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
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) {
@ -211,8 +247,8 @@ func TestSetCellValuesMultiByte(t *testing.T) {
func TestSetCellValue(t *testing.T) {
f := NewFile()
assert.EqualError(t, f.SetCellValue("Sheet1", "A", time.Now().UTC()), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
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.Now().UTC()))
assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.SetCellValue("Sheet1", "A", time.Duration(1e13)))
// Test set cell value with column and row style inherit
style1, err := f.NewStyle(&Style{NumFmt: 2})
assert.NoError(t, err)
@ -230,7 +266,7 @@ func TestSetCellValue(t *testing.T) {
assert.Equal(t, "0.50", B2)
// 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
f.SharedStrings = nil
f.Pkg.Store(defaultXMLPathSharedStrings, MacintoshCyrillicCharset)
@ -239,6 +275,59 @@ func TestSetCellValue(t *testing.T) {
f.WorkBook = nil
f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset)
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) {
@ -248,7 +337,7 @@ func TestSetCellValues(t *testing.T) {
v, err := f.GetCellValue("Sheet1", "A1")
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
err = f.SetCellValue("Sheet1", "A1", time.Date(1600, time.December, 31, 0, 0, 0, 0, time.UTC))
@ -261,9 +350,9 @@ func TestSetCellValues(t *testing.T) {
func TestSetCellBool(t *testing.T) {
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
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) {
@ -292,7 +381,7 @@ func TestGetCellValue(t *testing.T) {
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.checked = nil
f.checked = sync.Map{}
cells := []string{"A3", "A4", "B4", "A7", "B7"}
rows, err := f.GetRows("Sheet1")
assert.Equal(t, [][]string{nil, nil, {"A3"}, {"A4", "B4"}, nil, nil, {"A7", "B7"}, {"A8", "B8"}}, rows)
@ -308,35 +397,35 @@ func TestGetCellValue(t *testing.T) {
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.checked = nil
f.checked = sync.Map{}
cell, err := f.GetCellValue("Sheet1", "A2")
assert.Equal(t, "A2", cell)
assert.NoError(t, err)
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.checked = nil
f.checked = sync.Map{}
rows, err = f.GetRows("Sheet1")
assert.Equal(t, [][]string{nil, {"A2", "B2"}}, rows)
assert.NoError(t, err)
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.checked = nil
f.checked = sync.Map{}
rows, err = f.GetRows("Sheet1")
assert.Equal(t, [][]string{{"A1", "B1"}}, rows)
assert.NoError(t, err)
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.checked = nil
f.checked = sync.Map{}
rows, err = f.GetRows("Sheet1")
assert.Equal(t, [][]string{{"A3"}, {"A4", "B4"}, nil, nil, nil, nil, {"A7", "B7"}, {"A8", "B8"}}, rows)
assert.NoError(t, err)
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.checked = nil
f.checked = sync.Map{}
cell, err = f.GetCellValue("Sheet1", "H6")
assert.Equal(t, "H6", cell)
assert.NoError(t, err)
@ -351,6 +440,16 @@ func TestGetCellValue(t *testing.T) {
}, rows)
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.Pkg.Store("xl/worksheets/sheet1.xml", []byte(fmt.Sprintf(sheetData, `
<row r="1"><c r="A1"><v>2422.3000000000002</v></c></row>
@ -389,7 +488,7 @@ func TestGetCellValue(t *testing.T) {
<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="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")
assert.Equal(t, []string{
"2422.3",
@ -438,7 +537,7 @@ func TestGetCellValue(t *testing.T) {
assert.EqualError(t, value, "XML syntax error on line 1: invalid UTF-8")
// Test get cell value with invalid sheet name
_, err = f.GetCellValue("Sheet:1", "A1")
assert.EqualError(t, err, ErrSheetNameInvalid.Error())
assert.Equal(t, ErrSheetNameInvalid, err)
}
func TestGetCellType(t *testing.T) {
@ -451,10 +550,10 @@ func TestGetCellType(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, CellTypeSharedString, cellType)
_, 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
_, err = f.GetCellType("Sheet:1", "A1")
assert.EqualError(t, err, ErrSheetNameInvalid.Error())
assert.Equal(t, ErrSheetNameInvalid, err)
}
func TestGetValueFrom(t *testing.T) {
@ -480,7 +579,7 @@ func TestGetCellFormula(t *testing.T) {
// Test get cell formula with invalid sheet name
_, 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
assert.NoError(t, f.SetCellValue("Sheet1", "A1", true))
@ -508,6 +607,25 @@ func TestGetCellFormula(t *testing.T) {
formula, err := f.GetCellFormula("Sheet1", "B2")
assert.NoError(t, err)
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() {
@ -566,10 +684,10 @@ func TestSetCellFormula(t *testing.T) {
assert.NoError(t, f.SetCellFormula("Sheet1", "C19", "SUM(Sheet2!D2,Sheet2!D9)"))
// 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
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.Close())
@ -601,7 +719,7 @@ func TestSetCellFormula(t *testing.T) {
ref = "D1:D5"
assert.NoError(t, f.SetCellFormula("Sheet1", "D1", "=A1+C1", FormulaOpts{Ref: &ref, Type: &formulaType}))
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")))
// Test set table formula for the cells
@ -613,6 +731,14 @@ func TestSetCellFormula(t *testing.T) {
formulaType = STCellFormulaTypeDataTable
assert.NoError(t, f.SetCellFormula("Sheet1", "C2", "=SUM(Table1[[A]:[B]])", FormulaOpts{Type: &formulaType}))
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) {
@ -654,32 +780,54 @@ func TestGetCellRichText(t *testing.T) {
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")
// 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")
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")
assert.NoError(t, err)
assert.Equal(t, 0, len(runs))
// Test get cell rich text when string item index is negative
ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml")
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")
assert.NoError(t, err)
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
ws, ok = f.Sheet.Load("xl/worksheets/sheet1.xml")
assert.True(t, ok)
ws.(*xlsxWorksheet).SheetData.Row[0].C[0].V = "x"
_, err = f.GetCellRichText("Sheet1", "A1")
assert.EqualError(t, err, "strconv.Atoi: parsing \"x\": invalid syntax")
ws.(*xlsxWorksheet).SheetData.Row[0].C[0] = xlsxC{V: "x"}
runs, err = f.GetCellRichText("Sheet1", "A1")
assert.NoError(t, err)
assert.Equal(t, 0, len(runs))
// Test set cell rich text on not exists worksheet
_, err = f.GetCellRichText("SheetN", "A1")
assert.EqualError(t, err, "sheet SheetN does not exist")
// Test set cell rich text with illegal cell reference
_, 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
assert.NoError(t, f.SetCellRichText("Sheet1", "A1", []RichTextRun{{Font: &Font{ColorTheme: &theme}}}))
// Test set rich text color tint without theme
@ -696,7 +844,7 @@ func TestGetCellRichText(t *testing.T) {
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
// Test get cell rich text with invalid sheet name
_, err = f.GetCellRichText("Sheet:1", "A1")
assert.EqualError(t, err, ErrSheetNameInvalid.Error())
assert.Equal(t, ErrSheetNameInvalid, err)
}
func TestSetCellRichText(t *testing.T) {
@ -795,7 +943,7 @@ func TestSetCellRichText(t *testing.T) {
// Test set cell rich text with invalid sheet name
assert.EqualError(t, f.SetCellRichText("Sheet:1", "A1", richTextRun), ErrSheetNameInvalid.Error())
// 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)}}
// Test set cell rich text with characters over the maximum limit
assert.EqualError(t, f.SetCellRichText("Sheet1", "A1", richTextRun), ErrCellCharsLength.Error())
@ -927,6 +1075,16 @@ func TestFormattedValueNilWorkbookPr(t *testing.T) {
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) {
f, err := OpenFile(filepath.Join("test", "Book1.xlsx"), Options{UnzipXMLSizeLimit: 128})
assert.NoError(t, err)

135
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
// the LICENSE file.
//
@ -7,7 +7,7 @@
// 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.
// data. This library needs Go version 1.18 or later.
package excelize
@ -80,6 +80,30 @@ const (
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.
var (
chartView3DRotX = map[ChartType]int{
@ -474,7 +498,13 @@ var (
true: "r",
false: "l",
}
valTickLblPos = map[ChartType]string{
tickLblPosVal = map[ChartTickLabelPositionType]string{
ChartTickLabelNextToAxis: "nextTo",
ChartTickLabelHigh: "high",
ChartTickLabelLow: "low",
ChartTickLabelNone: "none",
}
tickLblPosNone = map[ChartType]string{
Contour: "none",
WireframeContour: "none",
}
@ -499,26 +529,42 @@ func parseChartOptions(opts *Chart) (*Chart, error) {
opts.Format.Locked = boolPtr(false)
}
if opts.Format.ScaleX == 0 {
opts.Format.ScaleX = defaultPictureScale
opts.Format.ScaleX = defaultDrawingScale
}
if opts.Format.ScaleY == 0 {
opts.Format.ScaleY = defaultPictureScale
opts.Format.ScaleY = defaultDrawingScale
}
if opts.Legend.Position == "" {
opts.Legend.Position = defaultChartLegendPosition
}
if opts.Title.Name == "" {
opts.Title.Name = " "
}
opts.parseTitle()
if opts.VaryColors == nil {
opts.VaryColors = boolPtr(true)
}
if opts.Border.Width == 0 {
opts.Border.Width = 0.75
}
if opts.ShowBlanksAs == "" {
opts.ShowBlanksAs = defaultChartShowBlanksAs
}
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
// set (such as offset, scale, aspect ratio setting and print settings) and
// 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",
// },
// },
// Title: excelize.ChartTitle{
// Name: "Fruit 3D Clustered Column Chart",
// Title: []excelize.RichTextRun{
// {
// Text: "Fruit 3D Clustered Column Chart",
// },
// },
// Legend: excelize.ChartLegend{
// ShowLegendKey: false,
@ -660,11 +708,11 @@ func parseChartOptions(opts *Chart) (*Chart, error) {
//
// Name
// Categories
// Sizes
// Values
// Fill
// Line
// Marker
// DataLabelPosition
//
// 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
@ -675,13 +723,15 @@ func parseChartOptions(opts *Chart) (*Chart, error) {
// 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.
//
// Sizes: This sets the bubble size in a data series.
//
// 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
// 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
// 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
// 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:
//
// Position
@ -727,7 +779,7 @@ func parseChartOptions(opts *Chart) (*Chart, error) {
//
// 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
// sheet name. The name property is optional. The default is to have no chart
// title.
@ -748,11 +800,11 @@ func parseChartOptions(opts *Chart) (*Chart, error) {
// Specifies that each data marker in the series has a different color by
// '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'.
//
// Set the position of the chart plot area by PlotArea. The properties that can
// be set are:
// Set the position of the chart plot area by 'PlotArea'. The properties that
// can be set are:
//
// SecondPlotValues
// ShowBubbleSize
@ -761,6 +813,7 @@ func parseChartOptions(opts *Chart) (*Chart, error) {
// ShowPercent
// ShowSerName
// ShowVal
// NumFmt
//
// SecondPlotValues: Specifies the values in second plot for the 'pieOfPie' and
// 'barOfPie' chart.
@ -783,6 +836,10 @@ func parseChartOptions(opts *Chart) (*Chart, error) {
// ShowVal: Specifies that the value shall be shown in a data label.
// 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'.
// The properties of 'XAxis' that can be set are:
//
@ -793,6 +850,7 @@ func parseChartOptions(opts *Chart) (*Chart, error) {
// ReverseOrder
// Maximum
// Minimum
// Alignment
// Font
// NumFmt
// Title
@ -807,6 +865,7 @@ func parseChartOptions(opts *Chart) (*Chart, error) {
// ReverseOrder
// Maximum
// Minimum
// Alignment
// Font
// LogBase
// NumFmt
@ -839,6 +898,24 @@ func parseChartOptions(opts *Chart) (*Chart, error) {
// Minimum: Specifies that the fixed minimum, 0 is auto. The 'Minimum' property
// 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
// properties of font that can be set are:
//
@ -863,6 +940,15 @@ func parseChartOptions(opts *Chart) (*Chart, error) {
// Set chart size by 'Dimension' property. The 'Dimension' property is optional.
// 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
// a single chart. For example, create a clustered column - line chart with
// data Sheet1!$E$1:$L$15:
@ -895,7 +981,7 @@ func parseChartOptions(opts *Chart) (*Chart, error) {
// }
// enable, disable := true, false
// if err := f.AddChart("Sheet1", "E1", &excelize.Chart{
// Type: "col",
// Type: excelize.Col,
// Series: []excelize.ChartSeries{
// {
// Name: "Sheet1!$A$2",
@ -912,8 +998,10 @@ func parseChartOptions(opts *Chart) (*Chart, error) {
// LockAspectRatio: false,
// Locked: &disable,
// },
// Title: excelize.ChartTitle{
// Name: "Clustered Column - Line Chart",
// Title: []excelize.RichTextRun{
// {
// Text: "Clustered Column - Line Chart",
// },
// },
// Legend: excelize.ChartLegend{
// Position: "left",
@ -927,7 +1015,7 @@ func parseChartOptions(opts *Chart) (*Chart, error) {
// ShowVal: true,
// },
// }, &excelize.Chart{
// Type: "line",
// Type: excelize.Line,
// Series: []excelize.ChartSeries{
// {
// Name: "Sheet1!$A$4",
@ -1097,7 +1185,8 @@ func (f *File) DeleteChart(sheet, cell string) error {
return err
}
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

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$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
@ -70,7 +70,7 @@ func TestChartSize(t *testing.T) {
var (
workdir decodeWsDr
anchor decodeTwoCellAnchor
anchor decodeCellAnchor
)
content, ok := newFile.Pkg.Load("xl/drawings/drawing1.xml")
@ -81,8 +81,8 @@ func TestChartSize(t *testing.T) {
t.FailNow()
}
err = xml.Unmarshal([]byte("<decodeTwoCellAnchor>"+
workdir.TwoCellAnchor[0].Content+"</decodeTwoCellAnchor>"), &anchor)
err = xml.Unmarshal([]byte("<decodeCellAnchor>"+
workdir.TwoCellAnchor[0].Content+"</decodeCellAnchor>"), &anchor)
if !assert.NoError(t, err) {
t.FailNow()
}
@ -120,13 +120,15 @@ func TestDeleteDrawing(t *testing.T) {
f := NewFile()
path := "xl/drawings/drawing1.xml"
f.Pkg.Store(path, MacintoshCyrillicCharset)
assert.EqualError(t, f.deleteDrawing(0, 0, path, "Chart"), "XML syntax error on line 1: invalid UTF-8")
f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
_, 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),
}}})
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")
}
func TestAddChart(t *testing.T) {
@ -156,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$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$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{
{
@ -174,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"}}
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$31", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$31:$D$31", Sizes: "Sheet1!$B$31:$D$31"},
{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$33", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$33:$D$33", Sizes: "Sheet1!$B$33:$D$33"},
{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$35", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$35:$D$35", Sizes: "Sheet1!$B$35:$D$35"},
{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$37", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$37:$D$37", Sizes: "Sheet1!$B$37:$D$37"},
{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", DataLabelPosition: ChartDataLabelsPositionLeft},
{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", DataLabelPosition: ChartDataLabelsPositionCenter},
{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", DataLabelPosition: ChartDataLabelsPositionInsideEnd},
{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", DataLabelPosition: ChartDataLabelsPositionRight},
}
format := GraphicOptions{
ScaleX: defaultPictureScale,
ScaleY: defaultPictureScale,
ScaleX: defaultDrawingScale,
ScaleY: defaultDrawingScale,
OffsetX: 15,
OffsetY: 10,
PrintObject: boolPtr(true),
@ -201,74 +208,75 @@ func TestAddChart(t *testing.T) {
ShowPercent: true,
ShowSerName: true,
ShowVal: true,
Fill: Fill{Type: "pattern", Pattern: 1},
}
for _, c := range []struct {
sheetName, cell string
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"}, 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: "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: "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: "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: "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: "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: "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: "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: "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: "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: "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: "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: "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: "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: "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: "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: "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: "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: "P45", opts: &Chart{Type: Col3D, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "3D Column Chart"}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
{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: "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: "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: "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: "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: "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: "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: []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: []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: []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: []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: []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: []RichTextRun{{Text: "Radar Chart"}}, PlotArea: plotArea, ShowBlanksAs: "span"}},
{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: []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: []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: []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: []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: []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: []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: []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: []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: []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: []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: []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: []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: []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: []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: []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: []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: []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: []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
{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: "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: "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: "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: "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: "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, Secondary: true, Minimum: &zero}, YAxis: ChartAxis{ReverseOrder: true, Minimum: &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: []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: []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: []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: []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: []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
{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: "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: "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: "AN16", opts: &Chart{Type: Area3D, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "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: "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: "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: []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: []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: []RichTextRun{{Text: "3D 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: []RichTextRun{{Text: "3D 100% Stacked Area Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
// 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: "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: "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: "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: []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: []RichTextRun{{Text: "3D Bar Cylinder Percent Stacked Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
// 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: "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: "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: "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: "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: "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: "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: []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: []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: []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: []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: []RichTextRun{{Text: "3D Bar Pyramid Percent Stacked Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero"}},
// 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: "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: "AV32", opts: &Chart{Type: Contour, Series: series, Format: format, Legend: legend, Title: ChartTitle{Name: "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: "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: []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: []RichTextRun{{Text: "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
{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: "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: "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: []RichTextRun{{Text: "Bubble 3D Chart"}}, PlotArea: plotArea, ShowBlanksAs: "zero", XAxis: ChartAxis{MajorGridLines: true}, YAxis: ChartAxis{MajorGridLines: true}}},
// 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
{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))
}
@ -280,32 +288,32 @@ func TestAddChart(t *testing.T) {
{"I1", Doughnut, "Clustered Column - Doughnut Chart"},
}
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}, YAxis: ChartAxis{Secondary: 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{}{
"A16": {Line, "Stacked Area - Line Chart"},
"I16": {Doughnut, "Stacked Area - Doughnut Chart"},
}
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")))
// Test with invalid sheet name
assert.EqualError(t, f.AddChart("Sheet:1", "A1", &Chart{Type: Col, Series: series[:1]}), ErrSheetNameInvalid.Error())
// 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
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
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
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())
// Test add chart with unsupported charset content types.
f.ContentTypes = nil
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) {
@ -323,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$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
var sheetIdx int
for idx, sheetName := range f.GetSheetList() {
@ -338,11 +346,11 @@ func TestAddChartSheet(t *testing.T) {
assert.EqualError(t, f.SetCellValue("Chart1", "A1", true), "sheet Chart1 is not a worksheet")
// 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
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
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())
@ -351,7 +359,7 @@ func TestAddChartSheet(t *testing.T) {
f = NewFile()
f.ContentTypes = nil
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) {
@ -369,8 +377,8 @@ func TestDeleteChart(t *testing.T) {
{Name: "Sheet1!$A$37", Categories: "Sheet1!$B$29:$D$29", Values: "Sheet1!$B$37:$D$37"},
}
format := GraphicOptions{
ScaleX: defaultPictureScale,
ScaleY: defaultPictureScale,
ScaleX: defaultDrawingScale,
ScaleY: defaultDrawingScale,
OffsetX: 15,
OffsetY: 10,
PrintObject: boolPtr(true),
@ -386,7 +394,7 @@ func TestDeleteChart(t *testing.T) {
ShowSerName: 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.SaveAs(filepath.Join("test", "TestDeleteChart.xlsx")))
// Test delete chart with invalid sheet name
@ -401,7 +409,7 @@ func TestDeleteChart(t *testing.T) {
}
func TestChartWithLogarithmicBase(t *testing.T) {
// Create test XLSX file with data
// Create test workbook with data
f := NewFile()
sheet1 := f.GetSheetName(0)
categories := map[string]float64{
@ -435,25 +443,25 @@ func TestChartWithLogarithmicBase(t *testing.T) {
cell string
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: "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: "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: "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: "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: "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: "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: []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: []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: []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: []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: []RichTextRun{{Text: "Line chart with log 1000 scaling"}}, YAxis: ChartAxis{LogBase: 1000}}},
} {
// Add two chart, one without and one with log scaling
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")))
// Write the XLSX file to a buffer
// Write the workbook to a buffer
var buffer bytes.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)
assert.NoError(t, err)

52
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
// the LICENSE file.
//
@ -7,7 +7,7 @@
// 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.
// data. This library needs Go version 1.18 or later.
package excelize
@ -18,7 +18,7 @@ import (
"strconv"
"strings"
"github.com/mohae/deepcopy"
"github.com/tiendc/go-deepcopy"
)
// 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 {
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 {
return rowIterator.cells, rowIterator.err
}
@ -290,7 +290,7 @@ func (f *File) GetColVisible(sheet, col string) (bool, error) {
//
// err := f.SetColVisible("Sheet1", "D:F", false)
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 {
return err
}
@ -301,8 +301,8 @@ func (f *File) SetColVisible(sheet, columns string, visible bool) error {
ws.mu.Lock()
defer ws.mu.Unlock()
colData := xlsxCol{
Min: min,
Max: max,
Min: minVal,
Max: maxVal,
Width: float64Ptr(defaultColWidth),
Hidden: !visible,
CustomWidth: true,
@ -354,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.
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, ":")
min, err = ColumnNameToNumber(colsTab[0])
minVal, err = ColumnNameToNumber(colsTab[0])
if err != nil {
return
}
max = min
maxVal = minVal
if len(colsTab) == 2 {
if max, err = ColumnNameToNumber(colsTab[1]); err != nil {
if maxVal, err = ColumnNameToNumber(colsTab[1]); err != nil {
return
}
}
if max < min {
min, max = max, min
if maxVal < minVal {
minVal, maxVal = maxVal, minVal
}
return
}
@ -427,7 +427,7 @@ func (f *File) SetColOutlineLevel(sheet, col string, level uint8) error {
//
// err = f.SetColStyle("Sheet1", "C:F", style)
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 {
return err
}
@ -453,10 +453,14 @@ func (f *File) SetColStyle(sheet, columns string, styleID int) error {
if ws.Cols == nil {
ws.Cols = &xlsxCols{}
}
width := defaultColWidth
if ws.SheetFormatPr != nil && ws.SheetFormatPr.DefaultColWidth > 0 {
width = ws.SheetFormatPr.DefaultColWidth
}
ws.Cols.Col = flatCols(xlsxCol{
Min: min,
Max: max,
Width: float64Ptr(defaultColWidth),
Min: minVal,
Max: maxVal,
Width: float64Ptr(width),
Style: styleID,
}, ws.Cols.Col, func(fc, c xlsxCol) xlsxCol {
fc.BestFit = c.BestFit
@ -470,7 +474,7 @@ func (f *File) SetColStyle(sheet, columns string, styleID int) error {
})
ws.mu.Unlock()
if rows := len(ws.SheetData.Row); rows > 0 {
for col := min; col <= max; col++ {
for col := minVal; col <= maxVal; col++ {
from, _ := CoordinatesToCellName(col, 1)
to, _ := CoordinatesToCellName(col, rows)
err = f.SetCellStyle(sheet, from, to, styleID)
@ -484,7 +488,7 @@ func (f *File) SetColStyle(sheet, columns string, styleID int) error {
//
// err := f.SetColWidth("Sheet1", "A", "H", 20)
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 {
return err
}
@ -501,8 +505,8 @@ func (f *File) SetColWidth(sheet, startCol, endCol string, width float64) error
ws.mu.Lock()
defer ws.mu.Unlock()
col := xlsxCol{
Min: min,
Max: max,
Min: minVal,
Max: maxVal,
Width: float64Ptr(width),
CustomWidth: true,
}
@ -529,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 {
var fc []xlsxCol
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
fc = append(fc, c)
}
@ -547,7 +552,8 @@ func flatCols(col xlsxCol, cols []xlsxCol, replacer func(fc, c xlsxCol) xlsxCol)
fc[idx] = replacer(fc[idx], column)
continue
}
c := deepcopy.Copy(column).(xlsxCol)
var c xlsxCol
deepcopy.Copy(&c, column)
c.Min, c.Max = i, i
fc = append(fc, c)
}

View File

@ -1,7 +1,9 @@
package excelize
import (
"fmt"
"path/filepath"
"sync"
"testing"
"github.com/stretchr/testify/assert"
@ -71,6 +73,17 @@ func TestCols(t *testing.T) {
cols.Next()
_, err = cols.Rows()
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) {
@ -124,12 +137,12 @@ func TestGetColsError(t *testing.T) {
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 = nil
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 = sync.Map{}
_, err = f.GetCols("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>`))
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")
assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
@ -139,7 +152,7 @@ func TestGetColsError(t *testing.T) {
cols.totalRows = 2
cols.totalCols = 2
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()
assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
@ -217,7 +230,7 @@ func TestColumnVisibility(t *testing.T) {
assert.Equal(t, true, visible)
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")
assert.EqualError(t, err, "sheet SheetN does not exist")
// Test get column visible with invalid sheet name
@ -353,6 +366,16 @@ func TestSetColStyle(t *testing.T) {
f.Styles = nil
f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset)
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) {

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
// the LICENSE file.
//
@ -7,7 +7,7 @@
// 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.
// data. This library needs Go version 1.18 or later.
package excelize
@ -676,7 +676,7 @@ func (c *cfb) writeUint64(value int) {
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) {
encoder := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder()
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
// the LICENSE file.
//
@ -7,7 +7,7 @@
// 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.
// data. This library needs Go version 1.18 or later.
package excelize
@ -39,7 +39,7 @@ func TestEncrypt(t *testing.T) {
assert.NoError(t, err)
raw[2050] = 3
_, err = Decrypt(raw, &Options{Password: "password"})
assert.EqualError(t, err, ErrUnsupportedEncryptMechanism.Error())
assert.Equal(t, ErrUnsupportedEncryptMechanism, err)
// Test encrypt spreadsheet with invalid password
assert.EqualError(t, f.SaveAs(filepath.Join("test", "Encryption.xlsx"), Options{Password: strings.Repeat("*", MaxFieldLength+1)}), ErrPasswordLengthInvalid.Error())

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
// the LICENSE file.
//
@ -7,12 +7,13 @@
// 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.
// data. This library needs Go version 1.18 or later.
package excelize
import (
"fmt"
"io"
"math"
"strings"
"unicode/utf16"
@ -24,14 +25,13 @@ type DataValidationType int
// Data validation types.
const (
_ DataValidationType = iota
typeNone // inline use
DataValidationTypeNone
DataValidationTypeCustom
DataValidationTypeDate
DataValidationTypeDecimal
typeList // inline use
DataValidationTypeList
DataValidationTypeTextLength
DataValidationTypeTime
// DataValidationTypeWhole Integer
DataValidationTypeWhole
)
@ -69,13 +69,41 @@ const (
DataValidationOperatorNotEqual
)
var (
// formulaEscaper mimics the Excel escaping rules for data validation,
// which converts `"` to `""` instead of `&quot;`.
var formulaEscaper = strings.NewReplacer(
formulaEscaper = strings.NewReplacer(
`&`, `&amp;`,
`<`, `&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.
@ -112,113 +140,92 @@ func (dv *DataValidation) SetInput(title, msg string) {
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 {
formula := strings.Join(keys, ",")
if MaxFieldLength < len(utf16.Encode([]rune(formula))) {
return ErrDataValidationFormulaLength
}
dv.Formula1 = fmt.Sprintf(`<formula1>"%s"</formula1>`, formulaEscaper.Replace(formula))
dv.Type = convDataValidationType(typeList)
dv.Type = dataValidationTypeMap[DataValidationTypeList]
if strings.HasPrefix(formula, "=") {
dv.Formula1 = formulaEscaper.Replace(formula)
return nil
}
dv.Formula1 = fmt.Sprintf(`"%s"`, strings.NewReplacer(`"`, `""`).Replace(formulaEscaper.Replace(formula)))
return nil
}
// 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 {
var formula1, formula2 string
switch v := f1.(type) {
genFormula := func(val interface{}) (string, error) {
var formula string
switch v := val.(type) {
case int:
formula1 = fmt.Sprintf("<formula1>%d</formula1>", v)
formula = fmt.Sprintf("%d", v)
case float64:
if math.Abs(v) > math.MaxFloat32 {
return ErrDataValidationRange
return formula, ErrDataValidationRange
}
formula1 = fmt.Sprintf("<formula1>%.17g</formula1>", v)
formula = fmt.Sprintf("%.17g", v)
case string:
formula1 = fmt.Sprintf("<formula1>%s</formula1>", v)
formula = v
default:
return ErrParameterInvalid
return formula, ErrParameterInvalid
}
switch v := f2.(type) {
case int:
formula2 = fmt.Sprintf("<formula2>%d</formula2>", v)
case float64:
if math.Abs(v) > math.MaxFloat32 {
return ErrDataValidationRange
return formula, nil
}
formula2 = fmt.Sprintf("<formula2>%.17g</formula2>", v)
case string:
formula2 = fmt.Sprintf("<formula2>%s</formula2>", v)
default:
return ErrParameterInvalid
formula1, err := genFormula(f1)
if err != nil {
return err
}
formula2, err := genFormula(f2)
if err != nil {
return err
}
dv.Formula1, dv.Formula2 = formula1, formula2
dv.Type = convDataValidationType(t)
dv.Operator = convDataValidationOperator(o)
return nil
dv.Type = dataValidationTypeMap[t]
dv.Operator = dataValidationOperatorMap[o]
return err
}
// SetSqrefDropList provides set data validation on a range with source
// reference range of the worksheet by given data validation object and
// worksheet name. The data validation object can be created by
// NewDataValidation function. 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:
// NewDataValidation function. There are limits to the number of items that
// will show in a data validation drop down list: The list can show up to show
// 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.Sqref = "A7:B8"
// dv.SetSqrefDropList("$E$1:$E$3")
// err := f.AddDataValidation("Sheet1", dv)
func (dv *DataValidation) SetSqrefDropList(sqref string) {
dv.Formula1 = fmt.Sprintf("<formula1>%s</formula1>", sqref)
dv.Type = convDataValidationType(typeList)
dv.Formula1 = sqref
dv.Type = dataValidationTypeMap[DataValidationTypeList]
}
// SetSqref provides function to set data validation range in drop list.
func (dv *DataValidation) SetSqref(sqref string) {
if dv.Sqref == "" {
dv.Sqref = sqref
} else {
return
}
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
// by given data validation object and worksheet name. The data validation
// object can be created by NewDataValidation function.
// by given data validation object and worksheet name. This function is
// concurrency safe. The data validation object can be created by
// NewDataValidation function.
//
// Example 1, set data validation on Sheet1!A1:B2 with validation criteria
// 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 {
return err
}
ws.mu.Lock()
defer ws.mu.Unlock()
if nil == ws.DataValidations {
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)
return err
}
@ -265,20 +294,83 @@ func (f *File) GetDataValidations(sheet string) ([]*DataValidation, error) {
if err != nil {
return nil, err
}
if ws.DataValidations == nil || len(ws.DataValidations.DataValidation) == 0 {
return nil, err
var (
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
// 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.
func (f *File) DeleteDataValidation(sheet string, sqref ...string) error {
ws, err := f.workSheetReader(sheet)
if err != nil {
return err
}
ws.mu.Lock()
defer ws.mu.Unlock()
if ws.DataValidations == nil {
return nil
}
@ -286,14 +378,14 @@ func (f *File) DeleteDataValidation(sheet string, sqref ...string) error {
ws.DataValidations = nil
return nil
}
delCells, err := f.flatSqref(sqref[0])
delCells, err := flatSqref(sqref[0])
if err != nil {
return err
}
dv := ws.DataValidations
for i := 0; i < len(dv.DataValidation); i++ {
var applySqref []string
colCells, err := f.flatSqref(dv.DataValidation[i].Sqref)
colCells, err := flatSqref(dv.DataValidation[i].Sqref)
if err != nil {
return err
}
@ -306,7 +398,7 @@ func (f *File) DeleteDataValidation(sheet string, sqref ...string) error {
}
}
for _, col := range colCells {
applySqref = append(applySqref, f.squashSqref(col)...)
applySqref = append(applySqref, squashSqref(col)...)
}
dv.DataValidation[i].Sqref = strings.Join(applySqref, " ")
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.
func (f *File) squashSqref(cells [][]int) []string {
func squashSqref(cells [][]int) []string {
if len(cells) == 1 {
cell, _ := CoordinatesToCellName(cells[0][0], cells[0][1])
return []string{cell}
} else if len(cells) == 0 {
return []string{}
}
var res []string
var refs []string
l, r := 0, 0
for i := 1; i < len(cells); i++ {
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 {
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
} else {
r++
}
}
curr, _ := f.coordinatesToRangeRef(append(cells[l], cells[r]...))
ref, _ := coordinatesToRangeRef(append(cells[l], cells[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
// the LICENSE file.
//
@ -7,11 +7,12 @@
// 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.
// data. This library needs Go version 1.18 or later.
package excelize
import (
"fmt"
"math"
"path/filepath"
"strings"
@ -71,6 +72,7 @@ func TestDataValidation(t *testing.T) {
dv.Sqref = "A5:B6"
for _, listValid := range [][]string{
{"1", "2", "3"},
{"=A1"},
{strings.Repeat("&", MaxFieldLength)},
{strings.Repeat("\u4E00", MaxFieldLength)},
{strings.Repeat("\U0001F600", 100), strings.Repeat("\u4E01", 50), "<&>"},
@ -82,7 +84,7 @@ func TestDataValidation(t *testing.T) {
assert.NotEqual(t, "", dv.Formula1,
"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))
dataValidations, err = f.GetDataValidations("Sheet1")
@ -103,6 +105,31 @@ func TestDataValidation(t *testing.T) {
dataValidations, err = f.GetDataValidations("Sheet1")
assert.NoError(t, err)
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) {

32
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
// the LICENSE file.
//
@ -7,7 +7,7 @@
// 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.
// data. This library needs Go version 1.18 or later.
package excelize
@ -214,3 +214,31 @@ func formatYear(y int) int {
}
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 min := 0; min < 60; min++ {
for minVal := 0; minVal < 60; minVal++ {
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
excel1900Time, err := timeToExcelTime(date, false)
assert.NoError(t, err)
date1900Out := timeFromExcelTime(excel1900Time, false)
assert.EqualValues(t, hour, date1900Out.Hour())
assert.EqualValues(t, min, date1900Out.Minute())
assert.EqualValues(t, minVal, date1900Out.Minute())
assert.EqualValues(t, sec, date1900Out.Second())
// Test use 1904 date system
excel1904Time, err := timeToExcelTime(date, true)
assert.NoError(t, err)
date1904Out := timeFromExcelTime(excel1904Time, true)
assert.EqualValues(t, hour, date1904Out.Hour())
assert.EqualValues(t, min, date1904Out.Minute())
assert.EqualValues(t, minVal, date1904Out.Minute())
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
// the LICENSE file.
//
@ -7,7 +7,7 @@
// 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.
// data. This library needs Go version 1.18 or later.
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
// the LICENSE file.
//
@ -7,7 +7,7 @@
// 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.
// data. This library needs Go version 1.18 or later.
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
// the LICENSE file.
//
@ -7,7 +7,7 @@
// 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.
// data. This library needs Go version 1.18 or later.
package excelize
@ -38,3 +38,12 @@ func TestDrawingParser(t *testing.T) {
_, _, err = f.drawingParser("wsDr")
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, "")
}

485
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
// the LICENSE file.
//
@ -7,7 +7,7 @@
// 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.
// data. This library needs Go version 1.18 or later.
package excelize
@ -16,16 +16,210 @@ import (
"fmt"
)
// newInvalidColumnNameError defined the error message on receiving the
// invalid column name.
func newInvalidColumnNameError(col string) error {
return fmt.Errorf("invalid column name %q", col)
var (
// ErrAddVBAProject defined the error message on add the VBA project in
// the workbook.
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
// row number.
func newInvalidRowNumberError(row int) error {
return fmt.Errorf("invalid row number %d", row)
// Error returns the error message on receiving the non existing sheet name.
func (err ErrSheetNotExist) Error() string {
return fmt.Sprintf("sheet %s does not exist", err.SheetName)
}
// 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
@ -34,18 +228,96 @@ func newInvalidCellNameError(cell string) error {
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
// with negative values.
func newInvalidExcelDateError(dateValue float64) error {
return fmt.Errorf("invalid date value %f, negative values are not supported", dateValue)
}
// 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.
func newNoExistTableError(name string) error {
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
// type are unsupported.
func newUnsupportedChartType(chartType ChartType) error {
@ -58,201 +330,8 @@ func newUnzipSizeLimitError(unzipSizeLimit int64) error {
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
// index.
func newViewIdxError(viewIndex int) error {
return fmt.Errorf("view index %d out of range", viewIndex)
}
// newUnknownFilterTokenError defined the error message on receiving a unknown
// filter operator token.
func newUnknownFilterTokenError(token string) error {
return fmt.Errorf("unknown operator: %s", token)
}
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 = fmt.Errorf("file path length exceeds maximum limit %d characters", MaxFilePathLength)
// 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)
// 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)
// ErrExistsTableName defined the error message on given table already exists.
ErrExistsTableName = errors.New("the same name table already exists")
// 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
// the LICENSE file.
@ -7,7 +7,7 @@
// 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.
// data. This library needs Go version 1.18 or later.
//
// See https://xuri.me/excelize for more information about this package.
package excelize
@ -29,31 +29,34 @@ import (
// File define a populated spreadsheet file struct.
type File struct {
mu sync.Mutex
checked sync.Map
formulaChecked bool
options *Options
xmlAttr map[string][]xml.Attr
checked map[string]bool
sharedStringItem [][]uint
sharedStringsMap map[string]int
sharedStringTemp *os.File
sheetMap map[string]string
streams map[string]*StreamWriter
tempFiles sync.Map
sharedStringsMap map[string]int
sharedStringItem [][]uint
sharedStringTemp *os.File
xmlAttr sync.Map
CalcChain *xlsxCalcChain
CharsetReader charsetTranscoderFn
Comments map[string]*xlsxComments
ContentTypes *xlsxTypes
DecodeVMLDrawing map[string]*decodeVmlDrawing
DecodeCellImages *decodeCellImages
Drawings sync.Map
Path string
Pkg sync.Map
Relationships sync.Map
SharedStrings *xlsxSST
Sheet sync.Map
SheetCount int
Styles *xlsxStyleSheet
Theme *xlsxTheme
DecodeVMLDrawing map[string]*decodeVmlDrawing
Theme *decodeTheme
VMLDrawing map[string]*vmlDrawing
VolatileDeps *xlsxVolTypes
WorkBook *xlsxWorkbook
Relationships sync.Map
Pkg sync.Map
CharsetReader charsetTranscoderFn
}
// charsetTranscoderFn set user-defined codepage transcoder function for open
@ -133,8 +136,8 @@ func OpenFile(filename string, opts ...Options) (*File, error) {
func newFile() *File {
return &File{
options: &Options{UnzipSizeLimit: UnzipSizeLimit, UnzipXMLSizeLimit: StreamChunkSize},
xmlAttr: make(map[string][]xml.Attr),
checked: make(map[string]bool),
xmlAttr: sync.Map{},
checked: sync.Map{},
sheetMap: make(map[string]string),
tempFiles: sync.Map{},
Comments: make(map[string]*xlsxComments),
@ -177,7 +180,7 @@ func OpenReader(r io.Reader, opts ...Options) (*File, error) {
return nil, err
}
f := newFile()
f.options = getOptions(opts...)
f.options = f.getOptions(opts...)
if err = f.checkOpenReaderOptions(); err != nil {
return nil, err
}
@ -216,8 +219,8 @@ func OpenReader(r io.Reader, opts ...Options) (*File, error) {
// getOptions provides a function to parse the optional settings for open
// and reading spreadsheet.
func getOptions(opts ...Options) *Options {
options := &Options{}
func (f *File) getOptions(opts ...Options) *Options {
options := f.options
for _, opt := range opts {
options = &opt
}
@ -225,7 +228,7 @@ func getOptions(opts ...Options) *Options {
}
// 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 }
// Creates new XML decoder with charset reader.
@ -239,15 +242,18 @@ func (f *File) xmlNewDecoder(rdr io.Reader) (ret *xml.Decoder) {
// time.Time type cell value by given worksheet name, cell reference and
// number format code.
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 {
return err
}
if s == 0 {
style, _ := f.NewStyle(&Style{NumFmt: format})
err = f.SetCellStyle(sheet, cell, cell, style)
if styleIdx == 0 {
styleIdx, _ = f.NewStyle(&Style{NumFmt: format})
} 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
@ -261,7 +267,7 @@ func (f *File) workSheetReader(sheet string) (ws *xlsxWorksheet, err error) {
return
}
if name, ok = f.getSheetXMLPath(sheet); !ok {
err = newNoExistSheetError(sheet)
err = ErrSheetNotExist{sheet}
return
}
if worksheet, ok := f.Sheet.Load(name); ok && worksheet != nil {
@ -275,24 +281,25 @@ func (f *File) workSheetReader(sheet string) (ws *xlsxWorksheet, err error) {
}
}
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))))
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)))).
Decode(ws); err != nil && err != io.EOF {
return
}
err = nil
if f.checked == nil {
f.checked = make(map[string]bool)
}
if ok = f.checked[name]; !ok {
if _, ok = f.checked.Load(name); !ok {
ws.checkSheet()
if err = ws.checkRow(); err != nil {
return
}
f.checked[name] = true
f.checked.Store(name, true)
}
f.Sheet.Store(name, ws)
return
@ -301,54 +308,65 @@ func (f *File) workSheetReader(sheet string) (ws *xlsxWorksheet, err error) {
// checkSheet provides a function to fill each row element and make that is
// continuous in a worksheet of XML.
func (ws *xlsxWorksheet) checkSheet() {
var row int
var r0 xlsxRow
for i, r := range ws.SheetData.Row {
if i == 0 && r.R == 0 {
r0 = r
ws.SheetData.Row = ws.SheetData.Row[1:]
var (
row int
r0Rows []xlsxRow
lastRowNum = func(r xlsxRow) int {
var num int
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
}
if r.R != 0 && r.R > row {
row = r.R
continue
}
if r.R != row {
row++
}
}
sheetData := xlsxSheetData{Row: make([]xlsxRow, row)}
row = 0
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 {
sheetData.Row[r.R-1] = r
row = r.R
continue
}
row++
r.R = row
sheetData.Row[row-1] = r
}
for _, r0Row := range r0Rows {
sheetData.Row[r0Row.R-1].R = r0Row.R
ws.checkSheetR0(&sheetData, &r0Row, true)
}
for i := 1; i <= row; i++ {
sheetData.Row[i-1].R = i
ws.checkSheetR0(&sheetData, &sheetData.Row[i-1], false)
}
ws.checkSheetR0(&sheetData, &r0)
}
// 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
// which cell is empty in the normal rows.
func (ws *xlsxWorksheet) checkSheetR0(sheetData *xlsxSheetData, r0 *xlsxRow) {
for _, cell := range r0.C {
if col, row, err := CellNameToCoordinates(cell.R); err == nil {
rows, rowIdx := len(sheetData.Row), row-1
for r := rows; r < row; r++ {
sheetData.Row = append(sheetData.Row, xlsxRow{R: r + 1})
}
func (ws *xlsxWorksheet) checkSheetR0(sheetData *xlsxSheetData, rowData *xlsxRow, r0 bool) {
checkRow := func(col, row int, r0 bool, cell xlsxC) {
rowIdx := row - 1
columns, colIdx := len(sheetData.Row[rowIdx].C), col-1
for c := columns; c < col; c++ {
sheetData.Row[rowIdx].C = append(sheetData.Row[rowIdx].C, xlsxC{})
@ -356,6 +374,19 @@ func (ws *xlsxWorksheet) checkSheetR0(sheetData *xlsxSheetData, r0 *xlsxRow) {
if !sheetData.Row[rowIdx].C[colIdx].hasValue() {
sheetData.Row[rowIdx].C[colIdx] = cell
}
if r0 {
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
@ -425,14 +456,14 @@ func (f *File) addRels(relPath, relType, target, targetMode string) int {
// UpdateLinkedValue fix linked values within a spreadsheet are not updating in
// Office Excel application. This function will be remove value tag when met a
// 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
// and generate a new value and will prompt to save the file or not.
//
// For example:
//
// <row r="19" spans="2:2">
// <row r="19">
// <c r="B19">
// <f>SUM(Sheet2!D2,Sheet2!D11)</f>
// <v>100</v>
@ -441,7 +472,7 @@ func (f *File) addRels(relPath, relType, target, targetMode string) int {
//
// to
//
// <row r="19" spans="2:2">
// <row r="19">
// <c r="B19">
// <f>SUM(Sheet2!D2,Sheet2!D11)</f>
// </c>
@ -561,3 +592,77 @@ func (f *File) setContentTypePartProjectExtensions(contentType string) error {
}
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"
"strconv"
"strings"
"sync"
"testing"
"time"
@ -207,6 +208,30 @@ func TestSaveFile(t *testing.T) {
assert.NoError(t, err)
assert.NoError(t, f.Save())
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) {
@ -364,11 +389,11 @@ func TestNewFile(t *testing.T) {
f := NewFile()
_, err := f.NewSheet("Sheet1")
assert.NoError(t, err)
_, err = f.NewSheet("XLSXSheet2")
_, err = f.NewSheet("Sheet2")
assert.NoError(t, err)
_, err = f.NewSheet("XLSXSheet3")
_, err = f.NewSheet("Sheet3")
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"))
f.SetActiveSheet(0)
@ -399,8 +424,8 @@ func TestSetCellHyperLink(t *testing.T) {
Tooltip: &tooltip,
}))
// Test set cell hyperlink with invalid sheet name
assert.EqualError(t, f.SetCellHyperLink("Sheet:1", "A1", "Sheet1!D60", "Location"), ErrSheetNameInvalid.Error())
assert.EqualError(t, f.SetCellHyperLink("Sheet2", "C3", "Sheet1!D8", ""), `invalid link type ""`)
assert.Equal(t, ErrSheetNameInvalid, f.SetCellHyperLink("Sheet:1", "A1", "Sheet1!D60", "Location"))
assert.Equal(t, newInvalidLinkTypeError(""), f.SetCellHyperLink("Sheet2", "C3", "Sheet1!D8", ""))
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.Close())
@ -430,6 +455,18 @@ func TestSetCellHyperLink(t *testing.T) {
assert.Equal(t, link, true)
assert.Equal(t, "https://github.com/xuri/excelize", target)
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) {
@ -741,10 +778,10 @@ func TestSetCellStyleNumberFormat(t *testing.T) {
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"}
expected := [][]string{
{"37947.7500001", "37948", "37947.75", "37,948", "37,947.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", "00:00.0", "37947.7500001", "37947.7500001"},
{"-37947.7500001", "-37948", "-37947.75", "-37,948", "-37,947.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", "-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", "12:10 AM", "12:10:05 AM", "00:10", "00:10:05", "12/30/99 00:10", "0 ", "0 ", "0.01 ", "0.01 ", "0.007", "0.007", "0.007", "0.007", "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", "24:00.0", "2.1", "2.1"},
{"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.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 ", "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/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"},
}
@ -833,11 +870,17 @@ func TestSetCellStyleCurrencyNumberFormat(t *testing.T) {
}
func TestSetCellStyleLangNumberFormat(t *testing.T) {
rawCellValues := [][]string{{"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}, {"45162"}}
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)
@ -849,7 +892,10 @@ func TestSetCellStyleLangNumberFormat(t *testing.T) {
// 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)
@ -961,7 +1007,7 @@ func TestSetDeleteSheet(t *testing.T) {
f, err := prepareTestBook3()
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")))
})
@ -1087,7 +1133,7 @@ func TestConditionalFormat(t *testing.T) {
{
Type: "cell",
Criteria: "between",
Format: format1,
Format: &format1,
MinValue: "6",
MaxValue: "8",
},
@ -1099,7 +1145,7 @@ func TestConditionalFormat(t *testing.T) {
{
Type: "cell",
Criteria: ">",
Format: format3,
Format: &format3,
Value: "6",
},
},
@ -1110,7 +1156,7 @@ func TestConditionalFormat(t *testing.T) {
{
Type: "top",
Criteria: "=",
Format: format3,
Format: &format3,
},
},
))
@ -1120,7 +1166,7 @@ func TestConditionalFormat(t *testing.T) {
{
Type: "unique",
Criteria: "=",
Format: format2,
Format: &format2,
},
},
))
@ -1130,7 +1176,7 @@ func TestConditionalFormat(t *testing.T) {
{
Type: "duplicate",
Criteria: "=",
Format: format2,
Format: &format2,
},
},
))
@ -1140,7 +1186,7 @@ func TestConditionalFormat(t *testing.T) {
{
Type: "top",
Criteria: "=",
Format: format1,
Format: &format1,
Value: "6",
Percent: true,
},
@ -1152,7 +1198,7 @@ func TestConditionalFormat(t *testing.T) {
{
Type: "average",
Criteria: "=",
Format: format3,
Format: &format3,
AboveAverage: true,
},
},
@ -1163,7 +1209,7 @@ func TestConditionalFormat(t *testing.T) {
{
Type: "average",
Criteria: "=",
Format: format1,
Format: &format1,
AboveAverage: false,
},
},
@ -1186,7 +1232,7 @@ func TestConditionalFormat(t *testing.T) {
{
Type: "formula",
Criteria: "L2<3",
Format: format1,
Format: &format1,
},
},
))
@ -1196,21 +1242,23 @@ func TestConditionalFormat(t *testing.T) {
{
Type: "cell",
Criteria: ">",
Format: format4,
Format: &format4,
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
assert.EqualError(t, f.SetConditionalFormat("SheetN", "L1:L10", nil), "sheet SheetN does not exist")
// 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"))
assert.NoError(t, err)
// 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{
{
Type: "",
@ -1222,7 +1270,7 @@ func TestConditionalFormat(t *testing.T) {
},
))
// 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{
{
Type: "data_bar",
@ -1236,7 +1284,7 @@ func TestConditionalFormat(t *testing.T) {
// Test create conditional format with invalid custom number format
var exp string
_, 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
f, err = OpenFile(filepath.Join("test", "Book1.xlsx"))
@ -1531,7 +1579,7 @@ func TestWorkSheetReader(t *testing.T) {
f = NewFile()
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.checked = nil
f.checked = sync.Map{}
_, err = f.workSheetReader("Sheet1")
assert.NoError(t, err)
}
@ -1607,13 +1655,13 @@ func prepareTestBook1() (*File, error) {
func prepareTestBook3() (*File, error) {
f := NewFile()
if _, err := f.NewSheet("XLSXSheet2"); err != nil {
if _, err := f.NewSheet("Sheet2"); err != nil {
return nil, err
}
if _, err := f.NewSheet("XLSXSheet3"); err != nil {
if _, err := f.NewSheet("Sheet3"); err != nil {
return nil, err
}
if err := f.SetCellInt("XLSXSheet2", "A23", 56); err != nil {
if err := f.SetCellInt("Sheet2", "A23", 56); err != nil {
return nil, err
}
if err := f.SetCellStr("Sheet1", "B20", "42"); err != nil {

53
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
// the LICENSE file.
//
@ -7,7 +7,7 @@
// 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.
// data. This library needs Go version 1.18 or later.
package excelize
@ -18,6 +18,7 @@ import (
"io"
"os"
"path/filepath"
"sort"
"strings"
"sync"
)
@ -49,7 +50,7 @@ func NewFile(opts ...Options) *File {
ws, _ := f.workSheetReader("Sheet1")
f.Sheet.Store("xl/worksheets/sheet1.xml", ws)
f.Theme, _ = f.themeReader()
f.options = getOptions(opts...)
f.options = f.getOptions(opts...)
return f
}
@ -176,6 +177,7 @@ func (f *File) writeToZip(zw *zip.Writer) error {
f.commentsWriter()
f.contentTypesWriter()
f.drawingsWriter()
f.volatileDepsWriter()
f.vmlDrawingWriter()
f.workBookWriter()
f.workSheetWriter()
@ -191,43 +193,48 @@ func (f *File) writeToZip(zw *zip.Writer) error {
return err
}
var from io.Reader
from, err = stream.rawData.Reader()
if err != nil {
if from, err = stream.rawData.Reader(); err != nil {
_ = stream.rawData.Close()
return err
}
_, err = io.Copy(fi, from)
if err != nil {
if _, err = io.Copy(fi, from); err != nil {
return err
}
}
var err error
var (
err error
files, tempFiles []string
)
f.Pkg.Range(func(path, content interface{}) bool {
if err != nil {
return false
}
if _, ok := f.streams[path.(string)]; ok {
return true
}
var fi io.Writer
fi, err = zw.Create(path.(string))
if err != nil {
return false
}
_, err = fi.Write(content.([]byte))
files = append(files, path.(string))
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 {
if _, ok := f.Pkg.Load(path); ok {
return true
}
var fi io.Writer
fi, err = zw.Create(path.(string))
if err != nil {
return false
}
_, err = fi.Write(f.readBytes(path.(string)))
tempFiles = append(tempFiles, path.(string))
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
}

25
go.mod
View File

@ -1,17 +1,22 @@
module github.com/xuri/excelize/v2
go 1.16
go 1.18
require (
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826
github.com/richardlehane/mscfb v1.0.4
github.com/stretchr/testify v1.8.0
github.com/xuri/efp v0.0.0-20230422071738-01f4e37c47e9
github.com/xuri/nfp v0.0.0-20230503010013-3f38cdbb0b83
golang.org/x/crypto v0.9.0
golang.org/x/image v0.5.0
golang.org/x/net v0.10.0
golang.org/x/text v0.9.0
github.com/stretchr/testify v1.9.0
github.com/tiendc/go-deepcopy v1.1.0
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7
golang.org/x/crypto v0.29.0
golang.org/x/image v0.18.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/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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
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/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.3/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/xuri/efp v0.0.0-20230422071738-01f4e37c47e9 h1:ge5g8vsTQclA5lXDi+PuiAFw5GMIlMHOB/5e1hsf96E=
github.com/xuri/efp v0.0.0-20230422071738-01f4e37c47e9/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
github.com/xuri/nfp v0.0.0-20230503010013-3f38cdbb0b83 h1:xVwnvkzzi+OiwhIkWOXvh1skFI6bagk8OvGuazM80Rw=
github.com/xuri/nfp v0.0.0-20230503010013-3f38cdbb0b83/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI=
golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4=
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.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
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.8.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.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
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=
github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00=
github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tiendc/go-deepcopy v1.1.0 h1:rBHhm5vg7WYnGLwktbQouodWjBXDoStOL4S7v/K8S4A=
github.com/tiendc/go-deepcopy v1.1.0/go.mod h1:toXoeQoUqXOOS/X4sKuiAoSk6elIdqc0pN7MTgOOo2I=
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7psK/lVsjIS2otl+1WyRyY=
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A=
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
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/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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

16
hsl.go
View File

@ -65,21 +65,21 @@ func RGBToHSL(r, g, b uint8) (h, s, l float64) {
fR := float64(r) / 255
fG := float64(g) / 255
fB := float64(b) / 255
max := math.Max(math.Max(fR, fG), fB)
min := math.Min(math.Min(fR, fG), fB)
l = (max + min) / 2
if max == min {
maxVal := math.Max(math.Max(fR, fG), fB)
minVal := math.Min(math.Min(fR, fG), fB)
l = (maxVal + minVal) / 2
if maxVal == minVal {
// Achromatic.
h, s = 0, 0
} else {
// Chromatic.
d := max - min
d := maxVal - minVal
if l > 0.5 {
s = d / (2.0 - max - min)
s = d / (2.0 - maxVal - minVal)
} else {
s = d / (max + min)
s = d / (maxVal + minVal)
}
switch max {
switch maxVal {
case fR:
h = (fG - fB) / d
if fG < fB {

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
// the LICENSE file.
//
@ -7,7 +7,7 @@
// 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.
// data. This library needs Go version 1.18 or later.
package excelize
@ -18,6 +18,7 @@ import (
"encoding/xml"
"fmt"
"io"
"math"
"math/big"
"os"
"regexp"
@ -48,16 +49,22 @@ func (f *File) ReadZipReader(r *zip.Reader) (map[string][]byte, int, error) {
fileName = partName
}
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)
}
if err == nil {
continue
}
}
if strings.HasPrefix(fileName, "xl/worksheets/sheet") {
if strings.HasPrefix(strings.ToLower(fileName), "xl/worksheets/sheet") {
worksheets++
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)
}
if err == nil {
continue
}
}
@ -225,12 +232,18 @@ func ColumnNumberToName(num int) (string, error) {
if num < MinColumns || num > MaxColumns {
return "", ErrColumnNumber
}
var col string
estimatedLength := 0
for n := num; n > 0; n = (n - 1) / 26 {
estimatedLength++
}
result := make([]byte, estimatedLength)
for num > 0 {
col = string(rune((num-1)%26+65)) + col
estimatedLength--
result[estimatedLength] = byte((num-1)%26 + 'A')
num = (num - 1) / 26
}
return col, nil
return string(result), nil
}
// 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
func CoordinatesToCellName(col, row int, abs ...bool) (string, error) {
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 := ""
for _, a := range abs {
@ -313,7 +329,7 @@ func sortCoordinates(coordinates []int) error {
// coordinatesToRangeRef provides a function to convert a pair of coordinates
// 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 {
return "", ErrCoordinates
}
@ -329,7 +345,7 @@ func (f *File) coordinatesToRangeRef(coordinates []int, abs ...bool) (string, er
}
// 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
for _, definedName := range f.GetDefinedName() {
if definedName.Name == definedNameName {
@ -350,7 +366,7 @@ func (f *File) getDefinedNameRefTo(definedNameName string, currentSheet string)
}
// 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
cells = make(map[int][][]int)
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.
func intPtr(i int) *int { return &i }
// uintPtr returns a pointer to an int with the given value.
func uintPtr(i uint) *uint { return &i }
// uintPtr returns a pointer to an unsigned integer with the given value.
func uintPtr(u uint) *uint { return &u }
// float64Ptr returns a pointer to a float64 with the given value.
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.
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
// serialization.
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
}
// 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
// Transitional namespaces.
func namespaceStrictToTransitional(content []byte) []byte {
@ -584,6 +652,16 @@ func getRootElement(d *xml.Decoder) []xml.Attr {
case xml.StartElement:
tokenIdx++
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
}
}
@ -623,8 +701,8 @@ func getXMLNamespace(space string, attr []xml.Attr) string {
func (f *File) replaceNameSpaceBytes(path string, contentMarshal []byte) []byte {
sourceXmlns := []byte(`xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">`)
targetXmlns := []byte(templateNamespaceIDMap)
if attr, ok := f.xmlAttr[path]; ok {
targetXmlns = []byte(genXMLNamespace(attr))
if attrs, ok := f.xmlAttr.Load(path); ok {
targetXmlns = []byte(genXMLNamespace(attrs.([]xml.Attr)))
}
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
mc := false
ignore := -1
if attr, ok := f.xmlAttr[path]; ok {
for i, attribute := range attr {
if attribute.Name.Local == ns.Name.Local && attribute.Name.Space == ns.Name.Space {
if attrs, ok := f.xmlAttr.Load(path); ok {
for i, attr := range attrs.([]xml.Attr) {
if attr.Name.Local == ns.Name.Local && attr.Name.Space == ns.Name.Space {
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
}
if attribute.Name.Local == "mc" && attribute.Name.Space == "xmlns" {
if attr.Name.Local == "mc" && attr.Name.Space == "xmlns" {
mc = true
}
}
}
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 {
f.xmlAttr[path] = append(f.xmlAttr[path], SourceRelationshipCompatibility)
attrs = append(attrs.([]xml.Attr), SourceRelationshipCompatibility)
f.xmlAttr.Store(path, attrs)
}
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"},
Value: ns.Name.Local,
})
f.xmlAttr.Store(path, attrs)
return
}
f.setIgnorableNameSpace(path, ignore, ns)
@ -668,8 +753,10 @@ func (f *File) addNameSpaces(path string, ns xml.Attr) {
// by the given attribute.
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"}
if inStrSlice(strings.Fields(f.xmlAttr[path][index].Value), ns.Name.Local, true) == -1 && inStrSlice(ignorableNS, ns.Name.Local, true) != -1 {
f.xmlAttr[path][index].Value = strings.TrimSpace(fmt.Sprintf("%s %s", f.xmlAttr[path][index].Value, ns.Name.Local))
xmlAttrs, _ := f.xmlAttr.Load(path)
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
}
// 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.
type Stack struct {
list *list.List

View File

@ -218,14 +218,13 @@ func TestCoordinatesToCellName_Error(t *testing.T) {
}
func TestCoordinatesToRangeRef(t *testing.T) {
f := NewFile()
_, err := f.coordinatesToRangeRef([]int{})
_, err := coordinatesToRangeRef([]int{})
assert.EqualError(t, err, ErrCoordinates.Error())
_, err = f.coordinatesToRangeRef([]int{1, -1, 1, 1})
assert.EqualError(t, err, "invalid cell reference [1, -1]")
_, err = f.coordinatesToRangeRef([]int{1, 1, 1, -1})
assert.EqualError(t, err, "invalid cell reference [1, -1]")
ref, err := f.coordinatesToRangeRef([]int{1, 1, 1, 1})
_, err = coordinatesToRangeRef([]int{1, -1, 1, 1})
assert.Equal(t, newCoordinatesToCellNameError(1, -1), err)
_, err = coordinatesToRangeRef([]int{1, 1, 1, -1})
assert.Equal(t, newCoordinatesToCellNameError(1, -1), err)
ref, err := coordinatesToRangeRef([]int{1, 1, 1, 1})
assert.NoError(t, err)
assert.EqualValues(t, ref, "A1:A1")
}
@ -238,6 +237,12 @@ func TestInStrSlice(t *testing.T) {
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) {
bold := true
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())
}
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) {
s := []byte{0x01}
assert.EqualValues(t, s, bytesReplace(s, []byte{}, []byte{}, 0))
@ -275,13 +289,19 @@ func TestBytesReplace(t *testing.T) {
func TestGetRootElement(t *testing.T) {
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) {
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"}})
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) {

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
// the LICENSE file.
//
@ -7,7 +7,7 @@
// 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.
// data. This library needs Go version 1.18 or later.
package excelize
@ -49,16 +49,16 @@ func (mc *xlsxMergeCell) Rect() ([]int, error) {
// | |
// |A8(x3,y4) C8(x4,y4)|
// +------------------------+
func (f *File) MergeCell(sheet, hCell, vCell string) error {
rect, err := rangeRefToCoordinates(hCell + ":" + vCell)
func (f *File) MergeCell(sheet, topLeftCell, bottomRightCell string) error {
rect, err := rangeRefToCoordinates(topLeftCell + ":" + bottomRightCell)
if err != nil {
return err
}
// Correct the range reference, such correct C1:B3 to B1:C3.
_ = sortCoordinates(rect)
hCell, _ = CoordinatesToCellName(rect[0], rect[1])
vCell, _ = CoordinatesToCellName(rect[2], rect[3])
topLeftCell, _ = CoordinatesToCellName(rect[0], rect[1])
bottomRightCell, _ = CoordinatesToCellName(rect[2], rect[3])
ws, err := f.workSheetReader(sheet)
if err != nil {
@ -66,7 +66,18 @@ func (f *File) MergeCell(sheet, hCell, vCell string) error {
}
ws.mu.Lock()
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 {
ws.MergeCells.Cells = append(ws.MergeCells.Cells, &xlsxMergeCell{Ref: ref, rect: rect})
} else {
@ -82,14 +93,14 @@ func (f *File) MergeCell(sheet, hCell, vCell string) error {
// err := f.UnmergeCell("Sheet1", "D3", "E9")
//
// 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)
if err != nil {
return err
}
ws.mu.Lock()
defer ws.mu.Unlock()
rect1, err := rangeRefToCoordinates(hCell + ":" + vCell)
rect1, err := rangeRefToCoordinates(topLeftCell + ":" + bottomRightCell)
if err != nil {
return err
}
@ -128,8 +139,8 @@ func (f *File) UnmergeCell(sheet, hCell, vCell string) error {
return nil
}
// GetMergeCells provides a function to get all merged cells from a worksheet
// currently.
// GetMergeCells provides a function to get all merged cells from a specific
// worksheet.
func (f *File) GetMergeCells(sheet string) ([]MergeCell, error) {
var mergeCells []MergeCell
ws, err := f.workSheetReader(sheet)
@ -265,9 +276,9 @@ func mergeCell(cell1, cell2 *xlsxMergeCell) *xlsxMergeCell {
if rect1[3] < rect2[3] {
rect1[3], rect2[3] = rect2[3], rect1[3]
}
hCell, _ := CoordinatesToCellName(rect1[0], rect1[1])
vCell, _ := CoordinatesToCellName(rect1[2], rect1[3])
return &xlsxMergeCell{rect: rect1, Ref: hCell + ":" + vCell}
topLeftCell, _ := CoordinatesToCellName(rect1[0], rect1[1])
bottomRightCell, _ := CoordinatesToCellName(rect1[2], rect1[3])
return &xlsxMergeCell{rect: rect1, Ref: topLeftCell + ":" + bottomRightCell}
}
// 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
// example: "D4".
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

@ -80,6 +80,13 @@ func TestMergeCell(t *testing.T) {
assert.True(t, ok)
ws.(*xlsxWorksheet).MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{nil, nil}}
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) {

5990
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
// the LICENSE file.
//
@ -7,7 +7,7 @@
// 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.
// data. This library needs Go version 1.18 or later.
package excelize
@ -23,6 +23,18 @@ import (
"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
// the picture with default value.
func parseGraphicOptions(opts *GraphicOptions) *GraphicOptions {
@ -30,8 +42,8 @@ func parseGraphicOptions(opts *GraphicOptions) *GraphicOptions {
return &GraphicOptions{
PrintObject: boolPtr(true),
Locked: boolPtr(true),
ScaleX: defaultPictureScale,
ScaleY: defaultPictureScale,
ScaleX: defaultDrawingScale,
ScaleY: defaultDrawingScale,
}
}
if opts.PrintObject == nil {
@ -41,10 +53,10 @@ func parseGraphicOptions(opts *GraphicOptions) *GraphicOptions {
opts.Locked = boolPtr(true)
}
if opts.ScaleX == 0 {
opts.ScaleX = defaultPictureScale
opts.ScaleX = defaultDrawingScale
}
if opts.ScaleY == 0 {
opts.ScaleY = defaultPictureScale
opts.ScaleY = defaultDrawingScale
}
return opts
}
@ -52,7 +64,10 @@ func parseGraphicOptions(opts *GraphicOptions) *GraphicOptions {
// 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)
// 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
//
@ -110,41 +125,50 @@ func parseGraphicOptions(opts *GraphicOptions) *GraphicOptions {
// }
// }
//
// The optional parameter "AutoFit" specifies if you make image size auto-fits the
// cell, the default value of that is 'false'.
// The optional parameter "AltText" is used to add alternative text to a graph
// 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
// hyperlink "External" for website or "Location" for moving to one of the
// cells in this workbook. When the "HyperlinkType" is "Location",
// coordinates need to start with "#".
//
// The optional parameter "Positioning" defines two types of the position of an
// image in an Excel spreadsheet, "oneCell" (Move but don't size with
// cells) or "absolute" (Don't move or size with cells). If you don't set this
// parameter, the default positioning is 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%.
// The optional parameter "Positioning" defines 3 types of the position of a
// graph object in a spreadsheet: "oneCell" (Move but don't size with
// cells), "twoCell" (Move and size with cells), and "absolute" (Don't move or
// size with cells). If you don't set this parameter, the default positioning
// is to move and size with cells.
func (f *File) AddPicture(sheet, cell, name string, opts *GraphicOptions) error {
var err error
// 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
// picture format set (such as offset, scale, aspect ratio setting and print
// 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
// example:
// types: EMF, EMZ, GIF, JPEG, JPG, PNG, SVG, TIF, TIFF, WMF, and WMZ. 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
//
@ -206,12 +232,15 @@ func (f *File) AddPictureFromBytes(sheet, cell string, pic *Picture) error {
if !ok {
return ErrImgExt
}
if pic.InsertType != PictureInsertTypePlaceOverCells {
return ErrParameterInvalid
}
options := parseGraphicOptions(pic.Format)
img, _, err := image.DecodeConfig(bytes.NewReader(pic.File))
if err != nil {
return err
}
// Read sheet data.
// Read sheet data
f.mu.Lock()
ws, err := f.workSheetReader(sheet)
if err != nil {
@ -226,7 +255,18 @@ func (f *File) AddPictureFromBytes(sheet, cell string, pic *Picture) error {
drawingID, drawingXML = f.prepareDrawing(ws, drawingID, sheet, drawingXML)
drawingRels := "xl/drawings/_rels/drawing" + strconv.Itoa(drawingID) + ".xml.rels"
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.
if options.Hyperlink != "" && options.HyperlinkType != "" {
if options.HyperlinkType == "External" {
@ -246,29 +286,6 @@ func (f *File) AddPictureFromBytes(sheet, cell string, pic *Picture) error {
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.mu.Lock()
defer sheetRels.mu.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
// xl/worksheets/sheet%d.xml by given worksheet name and relationship index.
func (f *File) addSheetLegacyDrawing(sheet string, rID int) {
@ -278,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
// xl/worksheets/sheet%d.xml by given worksheet name and relationship index.
func (f *File) addSheetDrawing(sheet string, rID int) {
@ -303,23 +330,20 @@ func (f *File) addSheetPicture(sheet string, rID int) error {
// countDrawings provides a function to get drawing files count storage in the
// folder xl/drawings.
func (f *File) countDrawings() int {
var c1, c2 int
drawings := map[string]struct{}{}
f.Pkg.Range(func(k, v interface{}) bool {
if strings.Contains(k.(string), "xl/drawings/drawing") {
c1++
drawings[k.(string)] = struct{}{}
}
return true
})
f.Drawings.Range(func(rel, value interface{}) bool {
if strings.Contains(rel.(string), "xl/drawings/drawing") {
c2++
drawings[rel.(string)] = struct{}{}
}
return true
})
if c1 < c2 {
return c2
}
return c1
return len(drawings)
}
// addDrawingPicture provides a function to add picture by given sheet,
@ -330,6 +354,9 @@ func (f *File) addDrawingPicture(sheet, drawingXML, cell, ext string, rID, hyper
if err != nil {
return err
}
if opts.Positioning != "" && inStrSlice(supportedPositioning, opts.Positioning, true) == -1 {
return ErrParameterInvalid
}
width, height := img.Width, img.Height
if opts.AutoFit {
if width, height, col, row, err = f.drawingResize(sheet, cell, float64(width), float64(height), opts); err != nil {
@ -435,130 +462,6 @@ func (f *File) addMedia(file []byte, ext string) string {
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.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.
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.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 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.mu.Lock()
defer sheetRels.mu.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
// embed in spreadsheet by given worksheet and cell name. This function
// returns the image contents as []byte data types. This function is
@ -599,19 +502,54 @@ func (f *File) GetPictures(sheet, cell string) ([]Picture, error) {
}
f.mu.Unlock()
if ws.Drawing == nil {
return nil, err
return f.getCellImages(sheet, cell)
}
target := f.getSheetRelationshipsTargetByID(sheet, ws.Drawing.RID)
drawingXML := strings.ReplaceAll(target, "..", "xl")
drawingXML := strings.TrimPrefix(strings.ReplaceAll(target, "..", "xl"), "/")
drawingRelationships := strings.ReplaceAll(
strings.ReplaceAll(target, "../drawings", "xl/drawings/_rels"), ".xml", ".xml.rels")
strings.ReplaceAll(drawingXML, "xl/drawings", "xl/drawings/_rels"), ".xml", ".xml.rels")
imgs, err := f.getCellImages(sheet, cell)
if err != nil {
return nil, err
}
pics, err := f.getPicture(row, col, drawingXML, drawingRelationships)
if err != nil {
return nil, err
}
return append(imgs, pics...), err
}
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
// worksheet name and cell reference. Note that the image file won't be deleted
// from the document currently.
// worksheet name and cell reference.
func (f *File) DeletePicture(sheet, cell string) error {
col, row, err := CellNameToCoordinates(cell)
if err != nil {
@ -627,85 +565,128 @@ func (f *File) DeletePicture(sheet, cell string) error {
return err
}
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
// embed in spreadsheet by given coordinates and drawing relationships.
func (f *File) getPicture(row, col int, drawingXML, drawingRelationships string) (pics []Picture, err error) {
var (
wsDr *xlsxWsDr
ok bool
deWsDr *decodeWsDr
drawRel *xlsxRelationship
deTwoCellAnchor *decodeTwoCellAnchor
)
var wsDr *xlsxWsDr
if wsDr, _, err = f.drawingParser(drawingXML); err != nil {
return
}
if pics = f.getPicturesFromWsDr(row, col, drawingRelationships, wsDr); len(pics) > 0 {
return
}
deWsDr = new(decodeWsDr)
if err = f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(f.readXML(drawingXML)))).
Decode(deWsDr); err != nil && err != io.EOF {
return
}
err = nil
for _, anchor := range deWsDr.TwoCellAnchor {
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 {
drawRel = f.getDrawingRelationships(drawingRelationships, deTwoCellAnchor.Pic.BlipFill.Blip.Embed)
if _, ok = supportedImageTypes[strings.ToLower(filepath.Ext(drawRel.Target))]; ok {
pic := Picture{Extension: filepath.Ext(drawRel.Target), Format: &GraphicOptions{}}
if buffer, _ := f.Pkg.Load(strings.ReplaceAll(drawRel.Target, "..", "xl")); buffer != nil {
wsDr.mu.Lock()
defer wsDr.mu.Unlock()
cond := func(from *xlsxFrom) bool { return from.Col == col && from.Row == row }
cond2 := func(from *decodeFrom) bool { return from.Col == col && from.Row == row }
cb := func(a *xdrCellAnchor, r *xlsxRelationship) {
pic := Picture{Extension: filepath.Ext(r.Target), Format: &GraphicOptions{}, InsertType: PictureInsertTypePlaceOverCells}
if buffer, _ := f.Pkg.Load(filepath.ToSlash(filepath.Clean("xl/drawings/" + r.Target))); buffer != nil {
pic.File = buffer.([]byte)
pic.Format.AltText = deTwoCellAnchor.Pic.NvPicPr.CNvPr.Descr
pic.Format.AltText = a.Pic.NvPicPr.CNvPr.Descr
pics = append(pics, pic)
}
}
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))
}
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
}
// getPicturesFromWsDr provides a function to get picture base name and raw
// content in worksheet drawing by given coordinates and drawing
// relationships.
func (f *File) getPicturesFromWsDr(row, col int, drawingRelationships string, wsDr *xlsxWsDr) (pics []Picture) {
var (
ok bool
anchor *xdrCellAnchor
drawRel *xlsxRelationship
)
wsDr.mu.Lock()
defer wsDr.mu.Unlock()
for _, anchor = range wsDr.TwoCellAnchor {
// extractCellAnchor extract drawing object from cell anchor by giving drawing
// cell anchor, drawing relationships part path, conditional and callback
// function.
func (f *File) extractCellAnchor(anchor *xdrCellAnchor, drawingRelationships string,
cond func(from *xlsxFrom) bool, cb func(anchor *xdrCellAnchor, rels *xlsxRelationship),
cond2 func(from *decodeFrom) bool, cb2 func(anchor *decodeCellAnchor, rels *xlsxRelationship),
) {
var drawRel *xlsxRelationship
if anchor.GraphicFrame == "" {
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,
anchor.Pic.BlipFill.Blip.Embed); drawRel != nil {
if _, ok = supportedImageTypes[strings.ToLower(filepath.Ext(drawRel.Target))]; ok {
pic := Picture{Extension: filepath.Ext(drawRel.Target), Format: &GraphicOptions{}}
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)
}
}
if _, ok := supportedImageTypes[strings.ToLower(filepath.Ext(drawRel.Target))]; ok {
cb(anchor, drawRel)
}
}
}
}
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)
}
}
}
}
}
// getDrawingRelationships provides a function to get drawing relationships
// from xl/drawings/_rels/drawing%s.xml.rels by given file name and
@ -752,10 +733,7 @@ func (f *File) drawingResize(sheet, cell string, width, height float64, opts *Gr
if inMergeCell {
continue
}
if inMergeCell, err = f.checkCellInRangeRef(cell, mergeCell[0]); err != nil {
return
}
if inMergeCell {
if inMergeCell, err = f.checkCellInRangeRef(cell, mergeCell[0]); err == nil {
rng, _ = cellRefsToCoordinates(mergeCell.GetStartAxis(), mergeCell.GetEndAxis())
_ = sortCoordinates(rng)
}
@ -778,7 +756,244 @@ func (f *File) drawingResize(sheet, cell string, width, height float64, opts *Gr
asp := float64(cellHeight) / height
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)
w, h = int(width*opts.ScaleX), int(height*opts.ScaleY)
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"
"testing"
"github.com/stretchr/testify/assert"
_ "golang.org/x/image/bmp"
_ "golang.org/x/image/tiff"
"github.com/stretchr/testify/assert"
)
func BenchmarkAddPictureFromBytes(b *testing.B) {
@ -49,6 +48,7 @@ func TestAddPicture(t *testing.T) {
// 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", "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")
assert.NoError(t, err)
assert.NoError(t, f.SetRowHeight("AddPicture", 10, 30))
@ -59,17 +59,55 @@ func TestAddPicture(t *testing.T) {
// Test add picture to worksheet from bytes
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
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"}}))
for cell, ext := range map[string]string{"Q8": "gif", "Q15": "jpg", "Q22": "tif", "Q28": "bmp"} {
assert.NoError(t, f.AddPicture("Sheet1", cell, filepath.Join("test", "images", fmt.Sprintf("excel.%s", ext)), nil))
for _, preset := range [][]string{{"Q8", "gif"}, {"Q15", "jpg"}, {"Q22", "tif"}, {"Q28", "bmp"}} {
assert.NoError(t, f.AddPicture("Sheet1", preset[0], filepath.Join("test", "images", fmt.Sprintf("excel.%s", preset[1])), nil))
}
// Test write file to given path
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddPicture1.xlsx")))
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
f = NewFile()
f.ContentTypes = nil
@ -114,6 +152,7 @@ func TestGetPicture(t *testing.T) {
assert.NoError(t, err)
assert.Len(t, pics[0].File, 13233)
assert.Empty(t, pics[0].Format.AltText)
assert.Equal(t, PictureInsertTypePlaceOverCells, pics[0].InsertType)
f, err = prepareTestBook1()
if !assert.NoError(t, err) {
@ -129,7 +168,7 @@ func TestGetPicture(t *testing.T) {
// Try to get picture from a worksheet with illegal cell reference
_, 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
pics, err = f.GetPictures("Sheet3", "I9")
@ -169,6 +208,32 @@ func TestGetPicture(t *testing.T) {
assert.Len(t, pics, 0)
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
f = NewFile()
pics, err = f.GetPictures("Sheet1", "F22")
@ -179,12 +244,44 @@ func TestGetPicture(t *testing.T) {
// Test get pictures with unsupported charset
path := "xl/drawings/drawing1.xml"
f.Drawings.Delete(path)
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")
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
f.Drawings.Delete(path)
_, 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.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) {
@ -192,6 +289,8 @@ func TestAddDrawingPicture(t *testing.T) {
f := NewFile()
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())
// 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"
f.Pkg.Store(path, MacintoshCyrillicCharset)
@ -221,19 +320,60 @@ func TestAddPictureFromBytes(t *testing.T) {
func TestDeletePicture(t *testing.T) {
f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
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.AddPicture("Sheet1", "P1", filepath.Join("test", "images", "excel.jpg"), nil))
assert.NoError(t, f.DeletePicture("Sheet1", "P1"))
// Add same pictures on different worksheets
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.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
assert.EqualError(t, f.DeletePicture("SheetN", "A1"), "sheet SheetN does not exist")
// 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
assert.EqualError(t, f.DeletePicture("Sheet1", ""), newCellNameToCoordinatesError("", newInvalidCellNameError("")).Error())
assert.Equal(t, newCellNameToCoordinatesError("", newInvalidCellNameError("")), f.DeletePicture("Sheet1", ""))
assert.NoError(t, f.Close())
// Test delete picture on no chart worksheet
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) {
@ -243,11 +383,22 @@ func TestDrawingResize(t *testing.T) {
assert.EqualError(t, err, "sheet SheetN does not exist")
// Test calculate drawing resize with invalid coordinates
_, _, _, _, 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")
assert.True(t, ok)
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) {
@ -273,3 +424,175 @@ func TestAddContentTypePart(t *testing.T) {
f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
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
// the LICENSE file.
//
@ -7,15 +7,22 @@
// 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.
// data. This library needs Go version 1.18 or later.
package excelize
import (
"bytes"
"encoding/xml"
"fmt"
"io"
"path/filepath"
"reflect"
"strconv"
"strings"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
// PivotTableOptions directly maps the format settings of the pivot table.
@ -26,9 +33,14 @@ import (
// PivotStyleMedium1 - PivotStyleMedium28
// PivotStyleDark1 - PivotStyleDark28
type PivotTableOptions struct {
pivotTableSheetName string
pivotTableXML string
pivotCacheXML string
pivotSheetName string
pivotDataRange string
namedDataRange bool
DataRange string
PivotTableRange string
Name string
Rows []PivotTableField
Columns []PivotTableField
Data []PivotTableField
@ -39,6 +51,7 @@ type PivotTableOptions struct {
UseAutoFormatting bool
PageOverThenDown bool
MergeItem bool
ClassicLayout bool
CompactData bool
ShowError bool
ShowRowHeaders bool
@ -46,10 +59,16 @@ type PivotTableOptions struct {
ShowRowStripes bool
ShowColStripes bool
ShowLastColumn bool
FieldPrintTitles bool
ItemPrintTitles bool
PivotTableStyleName string
}
// 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
// field. The default value is sum. The possible values for this attribute
// are:
@ -66,24 +85,28 @@ type PivotTableOptions struct {
// Var
// Varp
//
// Name specifies the name of the data field. Maximum 255 characters
// are allowed in data field name, excess characters will be truncated.
// NumFmt specifies the number format ID of the data field, this filed only
// accepts built-in number format ID and does not support custom number format
// expression currently.
type PivotTableField struct {
Compact bool
Data string
Name string
Outline bool
ShowAll bool
InsertBlankRow bool
Subtotal string
DefaultSubtotal bool
NumFmt int
}
// 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
// fields at the same time.
//
// For example, create a pivot table on the range reference Sheet1!$G$2:$M$34
// with the range reference Sheet1!$A$1:$E$31 as the data source, summarize by
// sum for sales:
// For example, create a pivot table on the range reference Sheet1!G2:M34 with
// the range reference Sheet1!A1:E31 as the data source, summarize by sum for
// sales:
//
// package main
//
@ -115,8 +138,8 @@ type PivotTableField struct {
// f.SetCellValue("Sheet1", fmt.Sprintf("E%d", row), region[rand.Intn(4)])
// }
// if err := f.AddPivotTable(&excelize.PivotTableOptions{
// DataRange: "Sheet1!$A$1:$E$31",
// PivotTableRange: "Sheet1!$G$2:$M$34",
// DataRange: "Sheet1!A1:E31",
// PivotTableRange: "Sheet1!G2:M34",
// Rows: []excelize.PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
// Filter: []excelize.PivotTableField{{Data: "Region"}},
// Columns: []excelize.PivotTableField{{Data: "Type", DefaultSubtotal: true}},
@ -145,22 +168,20 @@ func (f *File) AddPivotTable(opts *PivotTableOptions) error {
pivotCacheID := f.countPivotCache() + 1
sheetRelationshipsPivotTableXML := "../pivotTables/pivotTable" + strconv.Itoa(pivotTableID) + ".xml"
pivotTableXML := strings.ReplaceAll(sheetRelationshipsPivotTableXML, "..", "xl")
pivotCacheXML := "xl/pivotCache/pivotCacheDefinition" + strconv.Itoa(pivotCacheID) + ".xml"
err = f.addPivotCache(pivotCacheXML, opts)
if err != nil {
opts.pivotTableXML = strings.ReplaceAll(sheetRelationshipsPivotTableXML, "..", "xl")
opts.pivotCacheXML = "xl/pivotCache/pivotCacheDefinition" + strconv.Itoa(pivotCacheID) + ".xml"
if err = f.addPivotCache(opts); err != nil {
return err
}
// 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)
pivotCacheRels := "xl/pivotTables/_rels/pivotTable" + strconv.Itoa(pivotTableID) + ".xml.rels"
// rId not used
_ = f.addRels(pivotCacheRels, SourceRelationshipPivotCache, fmt.Sprintf("../pivotCache/pivotCacheDefinition%d.xml", pivotCacheID), "")
err = f.addPivotTable(cacheID, pivotTableID, pivotTableXML, opts)
if err != nil {
if err = f.addPivotTable(cacheID, pivotTableID, opts); err != nil {
return err
}
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)
if err != nil {
return nil, "", fmt.Errorf("parameter 'PivotTableRange' parsing error: %s", err.Error())
return nil, "", newPivotTableRangeError(err.Error())
}
opts.pivotTableSheetName = pivotTableSheetName
dataRange := f.getDefinedNameRefTo(opts.DataRange, pivotTableSheetName)
if dataRange == "" {
dataRange = opts.DataRange
if len(opts.Name) > MaxFieldLength {
return nil, "", ErrNameLength
}
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 {
return nil, "", fmt.Errorf("parameter 'DataRange' parsing error: %s", err.Error())
return nil, "", newPivotTableDataRangeError(err.Error())
}
dataSheet, err := f.workSheetReader(dataSheetName)
if err != nil {
@ -196,7 +219,10 @@ func (f *File) parseFormatPivotTableSet(opts *PivotTableOptions) (*xlsxWorksheet
}
pivotTableSheetPath, ok := f.getSheetXMLPath(pivotTableSheetName)
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
}
@ -231,17 +257,16 @@ func (f *File) adjustRange(rangeStr string) (string, []int, error) {
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.
func (f *File) getPivotFieldsOrder(opts *PivotTableOptions) ([]string, error) {
func (f *File) getTableFieldsOrder(opts *PivotTableOptions) ([]string, error) {
var order []string
dataRange := f.getDefinedNameRefTo(opts.DataRange, opts.pivotTableSheetName)
if dataRange == "" {
dataRange = opts.DataRange
if err := f.getPivotTableDataRange(opts); err != nil {
return order, err
}
dataSheet, coordinates, err := f.adjustRange(dataRange)
dataSheet, coordinates, err := f.adjustRange(opts.pivotDataRange)
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++ {
coordinate, _ := CoordinatesToCellName(col, coordinates[1])
@ -249,83 +274,68 @@ func (f *File) getPivotFieldsOrder(opts *PivotTableOptions) ([]string, error) {
if err != nil {
return order, err
}
if name == "" {
return order, ErrParameterInvalid
}
order = append(order, name)
}
return order, nil
}
// 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
definedNameRef := true
dataRange := f.getDefinedNameRefTo(opts.DataRange, opts.pivotTableSheetName)
if dataRange == "" {
definedNameRef = false
dataRange = opts.DataRange
}
dataSheet, coordinates, err := f.adjustRange(dataRange)
dataSheet, coordinates, err := f.adjustRange(opts.pivotDataRange)
if err != nil {
return fmt.Errorf("parameter 'DataRange' parsing error: %s", err.Error())
return newPivotTableDataRangeError(err.Error())
}
// data range has been checked
order, _ := f.getPivotFieldsOrder(opts)
hCell, _ := CoordinatesToCellName(coordinates[0], coordinates[1])
vCell, _ := CoordinatesToCellName(coordinates[2], coordinates[3])
order, err := f.getTableFieldsOrder(opts)
if err != nil {
return newPivotTableDataRangeError(err.Error())
}
topLeftCell, _ := CoordinatesToCellName(coordinates[0], coordinates[1])
bottomRightCell, _ := CoordinatesToCellName(coordinates[2], coordinates[3])
pc := xlsxPivotCacheDefinition{
SaveData: false,
RefreshOnLoad: true,
CreatedVersion: pivotTableVersion,
RefreshedVersion: pivotTableVersion,
RefreshedVersion: pivotTableRefreshedVersion,
MinRefreshableVersion: pivotTableVersion,
CacheSource: &xlsxCacheSource{
Type: "worksheet",
WorksheetSource: &xlsxWorksheetSource{
Ref: hCell + ":" + vCell,
Ref: topLeftCell + ":" + bottomRightCell,
Sheet: dataSheet,
},
},
CacheFields: &xlsxCacheFields{},
}
if definedNameRef {
if opts.namedDataRange {
pc.CacheSource.WorksheetSource = &xlsxWorksheetSource{Name: opts.DataRange}
}
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{
Name: name,
SharedItems: &sharedItems,
SharedItems: &xlsxSharedItems{ContainsBlank: true, M: []xlsxMissing{{}}},
})
}
pc.CacheFields.Count = len(pc.CacheFields.CacheField)
pivotCache, err := xml.Marshal(pc)
f.saveFileList(pivotCacheXML, pivotCache)
f.saveFileList(opts.pivotCacheXML, pivotCache)
return err
}
// addPivotTable provides a function to create a pivot table by given pivot
// 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
_, coordinates, err := f.adjustRange(opts.PivotTableRange)
if err != nil {
return fmt.Errorf("parameter 'PivotTableRange' parsing error: %s", err.Error())
return newPivotTableRangeError(err.Error())
}
hCell, _ := CoordinatesToCellName(coordinates[0], coordinates[1])
vCell, _ := CoordinatesToCellName(coordinates[2], coordinates[3])
topLeftCell, _ := CoordinatesToCellName(coordinates[0], coordinates[1])
bottomRightCell, _ := CoordinatesToCellName(coordinates[2], coordinates[3])
pivotTableStyle := func() string {
if opts.PivotTableStyleName == "" {
@ -334,11 +344,11 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op
return opts.PivotTableStyleName
}
pt := xlsxPivotTableDefinition{
Name: fmt.Sprintf("Pivot Table%d", pivotTableID),
Name: opts.Name,
CacheID: cacheID,
RowGrandTotals: &opts.RowGrandTotals,
ColGrandTotals: &opts.ColGrandTotals,
UpdatedVersion: pivotTableVersion,
UpdatedVersion: pivotTableRefreshedVersion,
MinRefreshableVersion: pivotTableVersion,
ShowDrill: &opts.ShowDrill,
UseAutoFormatting: &opts.UseAutoFormatting,
@ -346,10 +356,13 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op
MergeItem: &opts.MergeItem,
CreatedVersion: pivotTableVersion,
CompactData: &opts.CompactData,
GridDropZones: opts.ClassicLayout,
ShowError: &opts.ShowError,
FieldPrintTitles: opts.FieldPrintTitles,
ItemPrintTitles: opts.ItemPrintTitles,
DataCaption: "Values",
Location: &xlsxLocation{
Ref: hCell + ":" + vCell,
Ref: topLeftCell + ":" + bottomRightCell,
FirstDataCol: 1,
FirstDataRow: 1,
FirstHeaderRow: 1,
@ -376,6 +389,14 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op
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
_ = f.addPivotFields(&pt, opts)
@ -390,7 +411,7 @@ func (f *File) addPivotTable(cacheID, pivotTableID int, pivotTableXML string, op
_ = f.addPivotDataFields(&pt, opts)
pivotTable, err := xml.Marshal(pt)
f.saveFileList(pivotTableXML, pivotTable)
f.saveFileList(opts.pivotTableXML, pivotTable)
return err
}
@ -454,6 +475,7 @@ func (f *File) addPivotDataFields(pt *xlsxPivotTableDefinition, opts *PivotTable
}
dataFieldsSubtotals := f.getPivotTableFieldsSubtotal(opts.Data)
dataFieldsName := f.getPivotTableFieldsName(opts.Data)
dataFieldsNumFmtID := f.getPivotTableFieldsNumFmtID(opts.Data)
for idx, dataField := range dataFieldsIndex {
if pt.DataFields == nil {
pt.DataFields = &xlsxDataFields{}
@ -462,6 +484,7 @@ func (f *File) addPivotDataFields(pt *xlsxPivotTableDefinition, opts *PivotTable
Name: dataFieldsName[idx],
Fld: dataField,
Subtotal: dataFieldsSubtotals[idx],
NumFmtID: dataFieldsNumFmtID[idx],
})
}
@ -525,10 +548,18 @@ func (f *File) addPivotColFields(pt *xlsxPivotTableDefinition, opts *PivotTableO
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
// row in the data region by given pivot table definition and option.
func (f *File) addPivotFields(pt *xlsxPivotTableDefinition, opts *PivotTableOptions) error {
order, err := f.getPivotFieldsOrder(opts)
order, err := f.getTableFieldsOrder(opts)
if err != nil {
return err
}
@ -542,23 +573,26 @@ func (f *File) addPivotFields(pt *xlsxPivotTableDefinition, opts *PivotTableOpti
} else {
items = append(items, &xlsxItem{T: "default"})
}
pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, &xlsxPivotField{
fld := &xlsxPivotField{
Name: f.getPivotTableFieldName(name, opts.Rows),
Axis: "axisRow",
DataField: inPivotTableField(opts.Data, name) != -1,
Compact: &rowOptions.Compact,
Outline: &rowOptions.Outline,
ShowAll: rowOptions.ShowAll,
InsertBlankRow: rowOptions.InsertBlankRow,
DefaultSubtotal: &rowOptions.DefaultSubtotal,
Items: &xlsxItems{
Count: len(items),
Item: items,
},
})
}
fld.setClassicLayout(opts.ClassicLayout)
pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, fld)
continue
}
if inPivotTableField(opts.Filter, name) != -1 {
pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, &xlsxPivotField{
fld := &xlsxPivotField{
Axis: "axisPage",
DataField: inPivotTableField(opts.Data, name) != -1,
Name: f.getPivotTableFieldName(name, opts.Columns),
@ -568,7 +602,9 @@ func (f *File) addPivotFields(pt *xlsxPivotTableDefinition, opts *PivotTableOpti
{T: "default"},
},
},
})
}
fld.setClassicLayout(opts.ClassicLayout)
pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, fld)
continue
}
if inPivotTableField(opts.Columns, name) != -1 {
@ -579,33 +615,41 @@ func (f *File) addPivotFields(pt *xlsxPivotTableDefinition, opts *PivotTableOpti
} else {
items = append(items, &xlsxItem{T: "default"})
}
pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, &xlsxPivotField{
fld := &xlsxPivotField{
Name: f.getPivotTableFieldName(name, opts.Columns),
Axis: "axisCol",
DataField: inPivotTableField(opts.Data, name) != -1,
Compact: &columnOptions.Compact,
Outline: &columnOptions.Outline,
ShowAll: columnOptions.ShowAll,
InsertBlankRow: columnOptions.InsertBlankRow,
DefaultSubtotal: &columnOptions.DefaultSubtotal,
Items: &xlsxItems{
Count: len(items),
Item: items,
},
})
}
fld.setClassicLayout(opts.ClassicLayout)
pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, fld)
continue
}
if inPivotTableField(opts.Data, name) != -1 {
pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, &xlsxPivotField{
fld := &xlsxPivotField{
DataField: true,
})
}
fld.setClassicLayout(opts.ClassicLayout)
pt.PivotFields.PivotField = append(pt.PivotFields.PivotField, fld)
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
}
// countPivotTables provides a function to get drawing files count storage in
// the folder xl/pivotTables.
// countPivotTables provides a function to get pivot table files count storage
// in the folder xl/pivotTables.
func (f *File) countPivotTables() int {
count := 0
f.Pkg.Range(func(k, v interface{}) bool {
@ -617,8 +661,8 @@ func (f *File) countPivotTables() int {
return count
}
// countPivotCache provides a function to get drawing files count storage in
// the folder xl/pivotCache.
// countPivotCache provides a function to get pivot table cache definition files
// count storage in the folder xl/pivotCache.
func (f *File) countPivotCache() int {
count := 0
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.
func (f *File) getPivotFieldsIndex(fields []PivotTableField, opts *PivotTableOptions) ([]int, error) {
var pivotFieldsIndex []int
orders, err := f.getPivotFieldsOrder(opts)
orders, err := f.getTableFieldsOrder(opts)
if err != nil {
return pivotFieldsIndex, err
}
@ -689,6 +733,22 @@ func (f *File) getPivotTableFieldName(name string, fields []PivotTableField) str
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.
func (f *File) getPivotTableFieldOptions(name string, fields []PivotTableField) (options PivotTableField, ok bool) {
for _, field := range fields {
@ -719,3 +779,318 @@ func (f *File) addWorkbookPivotCache(RID int) int {
})
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"
)
func TestAddPivotTable(t *testing.T) {
func TestPivotTable(t *testing.T) {
f := NewFile()
// Create some data in a sheet
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("E%d", row), region[rand.Intn(4)]))
}
assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!$A$1:$E$31",
PivotTableRange: "Sheet1!$G$2:$M$34",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
expected := &PivotTableOptions{
pivotTableXML: "xl/pivotTables/pivotTable1.xml",
pivotCacheXML: "xl/pivotCache/pivotCacheDefinition1.xml",
DataRange: "Sheet1!A1:E31",
PivotTableRange: "Sheet1!G2:M34",
Name: "PivotTable1",
Rows: []PivotTableField{{Data: "Month", ShowAll: true, DefaultSubtotal: true}, {Data: "Year"}},
Filter: []PivotTableField{{Data: "Region"}},
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
Data: []PivotTableField{{Data: "Sales", Subtotal: "Sum", Name: "Summarize by Sum"}},
Columns: []PivotTableField{{Data: "Type", ShowAll: true, InsertBlankRow: true, DefaultSubtotal: true}},
Data: []PivotTableField{{Data: "Sales", Subtotal: "Sum", Name: "Summarize by Sum", NumFmt: 38}},
RowGrandTotals: true,
ColGrandTotals: true,
ShowDrill: true,
ClassicLayout: true,
ShowError: true,
ShowRowHeaders: true,
ShowColHeaders: true,
ShowLastColumn: true,
ShowError: 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
assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!$A$1:$E$31",
PivotTableRange: "Sheet1!$U$34:$O$2",
DataRange: "Sheet1!A1:E31",
PivotTableRange: "Sheet1!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"}},
@ -54,10 +67,15 @@ func TestAddPivotTable(t *testing.T) {
ShowColHeaders: 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{
DataRange: "Sheet1!$A$1:$E$31",
PivotTableRange: "Sheet1!$W$2:$AC$34",
DataRange: "Sheet1!A1:E31",
PivotTableRange: "Sheet1!W2:AC34",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Columns: []PivotTableField{{Data: "Region"}},
Data: []PivotTableField{{Data: "Sales", Subtotal: "Count", Name: "Summarize by Count"}},
@ -69,8 +87,8 @@ func TestAddPivotTable(t *testing.T) {
ShowLastColumn: true,
}))
assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!$A$1:$E$31",
PivotTableRange: "Sheet1!$G$42:$W$55",
DataRange: "Sheet1!A1:E31",
PivotTableRange: "Sheet1!G42:W55",
Rows: []PivotTableField{{Data: "Month"}},
Columns: []PivotTableField{{Data: "Region", DefaultSubtotal: true}, {Data: "Year"}},
Data: []PivotTableField{{Data: "Sales", Subtotal: "CountNums", Name: "Summarize by CountNums"}},
@ -82,8 +100,8 @@ func TestAddPivotTable(t *testing.T) {
ShowLastColumn: true,
}))
assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!$A$1:$E$31",
PivotTableRange: "Sheet1!$AE$2:$AG$33",
DataRange: "Sheet1!A1:E31",
PivotTableRange: "Sheet1!AE2:AG33",
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"}},
RowGrandTotals: true,
@ -95,8 +113,8 @@ func TestAddPivotTable(t *testing.T) {
}))
// Create pivot table with empty subtotal field name and specified style
assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!$A$1:$E$31",
PivotTableRange: "Sheet1!$AJ$2:$AP1$35",
DataRange: "Sheet1!A1:E31",
PivotTableRange: "Sheet1!AJ2:AP135",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Filter: []PivotTableField{{Data: "Region"}},
Columns: []PivotTableField{},
@ -109,14 +127,14 @@ func TestAddPivotTable(t *testing.T) {
ShowLastColumn: true,
PivotTableStyleName: "PivotStyleLight19",
}))
_, err := f.NewSheet("Sheet2")
_, err = f.NewSheet("Sheet2")
assert.NoError(t, err)
assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!$A$1:$E$31",
PivotTableRange: "Sheet2!$A$1:$AR$15",
DataRange: "Sheet1!A1:E31",
PivotTableRange: "Sheet2!A1:AN17",
Rows: []PivotTableField{{Data: "Month"}},
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,
ColGrandTotals: true,
ShowDrill: true,
@ -124,12 +142,19 @@ func TestAddPivotTable(t *testing.T) {
ShowColHeaders: 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{
DataRange: "Sheet1!$A$1:$E$31",
PivotTableRange: "Sheet2!$A$18:$AR$54",
DataRange: "Sheet1!A1:E31",
PivotTableRange: "Sheet2!A20:AR60",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Type"}},
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,
ColGrandTotals: 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
assert.NoError(t, f.SetDefinedName(&DefinedName{
Name: "dataRange",
RefersTo: "Sheet1!$A$1:$E$31",
RefersTo: "Sheet1!A1:E31",
Comment: "Pivot Table Data Range",
Scope: "Sheet2",
}))
assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "dataRange",
PivotTableRange: "Sheet2!$A$57:$AJ$91",
PivotTableRange: "Sheet2!A65:AJ100",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
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,
ColGrandTotals: true,
ShowDrill: true,
@ -159,106 +184,123 @@ func TestAddPivotTable(t *testing.T) {
}))
// 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
assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!$A$1:$A$1",
PivotTableRange: "Sheet1!$U$34:$O$2",
assert.Equal(t, newPivotTableDataRangeError("parameter is invalid"), f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!A1:A1",
PivotTableRange: "Sheet1!U34:O2",
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 the data range of the worksheet that is not declared
assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "$A$1:$E$31",
PivotTableRange: "Sheet1!$U$34:$O$2",
assert.Equal(t, newPivotTableDataRangeError("parameter is invalid"), f.AddPivotTable(&PivotTableOptions{
DataRange: "A1:E31",
PivotTableRange: "Sheet1!U34:O2",
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 the worksheet declared in the data range does not exist
assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "SheetN!$A$1:$E$31",
PivotTableRange: "Sheet1!$U$34:$O$2",
assert.Equal(t, ErrSheetNotExist{"SheetN"}, f.AddPivotTable(&PivotTableOptions{
DataRange: "SheetN!A1:E31",
PivotTableRange: "Sheet1!U34:O2",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
Data: []PivotTableField{{Data: "Sales"}},
}), "sheet SheetN does not exist")
}))
// Test the pivot table range of the worksheet that is not declared
assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!$A$1:$E$31",
PivotTableRange: "$U$34:$O$2",
assert.Equal(t, newPivotTableRangeError("parameter is invalid"), f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!A1:E31",
PivotTableRange: "U34:O2",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
Data: []PivotTableField{{Data: "Sales"}},
}), `parameter 'PivotTableRange' parsing error: parameter is invalid`)
}))
// Test the worksheet declared in the pivot table range does not exist
assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!$A$1:$E$31",
PivotTableRange: "SheetN!$U$34:$O$2",
assert.Equal(t, ErrSheetNotExist{"SheetN"}, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!A1:E31",
PivotTableRange: "SheetN!U34:O2",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
Data: []PivotTableField{{Data: "Sales"}},
}), "sheet SheetN does not exist")
}))
// Test not exists worksheet in data range
assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "SheetN!$A$1:$E$31",
PivotTableRange: "Sheet1!$U$34:$O$2",
assert.Equal(t, ErrSheetNotExist{"SheetN"}, f.AddPivotTable(&PivotTableOptions{
DataRange: "SheetN!A1:E31",
PivotTableRange: "Sheet1!U34:O2",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
Data: []PivotTableField{{Data: "Sales"}},
}), "sheet SheetN does not exist")
}))
// Test invalid row number in data range
assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!$A$0:$E$31",
PivotTableRange: "Sheet1!$U$34:$O$2",
assert.Equal(t, newPivotTableDataRangeError(newCellNameToCoordinatesError("A0", newInvalidCellNameError("A0")).Error()), f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!A0:E31",
PivotTableRange: "Sheet1!U34:O2",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
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")))
// Test with field names that exceed the length limit and invalid subtotal
assert.NoError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!$A$1:$E$31",
PivotTableRange: "Sheet1!$G$2:$M$34",
DataRange: "Sheet1!A1:E31",
PivotTableRange: "Sheet1!G2:M34",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
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
assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet:1!$A$1:$E$31",
PivotTableRange: "Sheet:1!$G$2:$M$34",
assert.Error(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet:1!A1:E31",
PivotTableRange: "Sheet:1!G2:M34",
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
_, _, err = f.adjustRange("")
assert.EqualError(t, err, ErrParameterRequired.Error())
assert.Error(t, err, ErrParameterRequired)
// Test adjust range with incorrect range
_, _, err = f.adjustRange("sheet1!")
assert.EqualError(t, err, "parameter is invalid")
// Test get pivot fields order with empty data range
_, err = f.getPivotFieldsOrder(&PivotTableOptions{})
// Test get table fields order with empty data range
_, err = f.getTableFieldsOrder(&PivotTableOptions{})
assert.EqualError(t, err, `parameter 'DataRange' parsing error: parameter is required`)
// Test add pivot cache with empty data range
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")
assert.EqualError(t, f.addPivotCache(&PivotTableOptions{}), "parameter 'DataRange' parsing error: parameter is required")
// 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
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
assert.EqualError(t, f.addPivotFields(nil, &PivotTableOptions{
DataRange: "$A$1:$E$31",
PivotTableRange: "Sheet1!$U$34:$O$2",
DataRange: "A1:E31",
PivotTableRange: "Sheet1!U34:O2",
Rows: []PivotTableField{{Data: "Month", DefaultSubtotal: true}, {Data: "Year"}},
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
Data: []PivotTableField{{Data: "Sales"}},
@ -268,20 +310,145 @@ func TestAddPivotTable(t *testing.T) {
assert.EqualError(t, err, `parameter 'DataRange' parsing error: parameter is required`)
// Test add pivot table with unsupported charset content types.
f = NewFile()
assert.NoError(t, f.SetSheetRow("Sheet1", "A1", &[]string{"Month", "Year", "Type", "Sales", "Region"}))
f.ContentTypes = nil
f.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
assert.EqualError(t, f.AddPivotTable(&PivotTableOptions{
DataRange: "Sheet1!$A$1:$E$31",
PivotTableRange: "Sheet1!$G$2:$M$34",
DataRange: "Sheet1!A1:E31",
PivotTableRange: "Sheet1!G2:M34",
Rows: []PivotTableField{{Data: "Year"}},
}), "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) {
f := NewFile()
// Test invalid data range
assert.EqualError(t, f.addPivotRowFields(&xlsxPivotTableDefinition{}, &PivotTableOptions{
DataRange: "Sheet1!$A$1:$A$1",
DataRange: "Sheet1!A1:A1",
}), `parameter 'DataRange' parsing error: parameter is invalid`)
}
@ -289,7 +456,7 @@ func TestAddPivotPageFields(t *testing.T) {
f := NewFile()
// Test invalid data range
assert.EqualError(t, f.addPivotPageFields(&xlsxPivotTableDefinition{}, &PivotTableOptions{
DataRange: "Sheet1!$A$1:$A$1",
DataRange: "Sheet1!A1:A1",
}), `parameter 'DataRange' parsing error: parameter is invalid`)
}
@ -297,7 +464,7 @@ func TestAddPivotDataFields(t *testing.T) {
f := NewFile()
// Test invalid data range
assert.EqualError(t, f.addPivotDataFields(&xlsxPivotTableDefinition{}, &PivotTableOptions{
DataRange: "Sheet1!$A$1:$A$1",
DataRange: "Sheet1!A1:A1",
}), `parameter 'DataRange' parsing error: parameter is invalid`)
}
@ -305,19 +472,55 @@ func TestAddPivotColFields(t *testing.T) {
f := NewFile()
// Test invalid data range
assert.EqualError(t, f.addPivotColFields(&xlsxPivotTableDefinition{}, &PivotTableOptions{
DataRange: "Sheet1!$A$1:$A$1",
DataRange: "Sheet1!A1:A1",
Columns: []PivotTableField{{Data: "Type", DefaultSubtotal: true}},
}), `parameter 'DataRange' parsing error: parameter is invalid`)
}
func TestGetPivotFieldsOrder(t *testing.T) {
f := NewFile()
// Test get pivot fields order with not exist worksheet
_, err := f.getPivotFieldsOrder(&PivotTableOptions{DataRange: "SheetN!$A$1:$E$31"})
// Test get table fields order with not exist worksheet
_, err := f.getTableFieldsOrder(&PivotTableOptions{DataRange: "SheetN!A1:E31"})
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) {
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")
}

165
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
// the LICENSE file.
//
@ -7,22 +7,35 @@
// 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.
// data. This library needs Go version 1.18 or later.
package excelize
import (
"bytes"
"encoding/xml"
"fmt"
"io"
"math"
"os"
"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
// 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,
@ -50,19 +63,22 @@ func (f *File) GetRows(sheet string, opts ...Options) ([][]string, error) {
if err != nil {
return nil, err
}
results, cur, max := make([][]string, 0, 64), 0, 0
results, cur, maxVal := make([][]string, 0, 64), 0, 0
for rows.Next() {
cur++
row, err := rows.Columns(opts...)
if err != nil {
break
}
results = append(results, row)
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.
@ -138,7 +154,7 @@ func (rows *Rows) Columns(opts ...Options) ([]string, error) {
}
var rowIterator rowXMLIterator
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 {
return rowIterator.cells, rowIterator.err
}
@ -202,15 +218,6 @@ func appendSpace(l int, s []string) []string {
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.
type rowXMLIterator struct {
err error
@ -346,8 +353,10 @@ func (f *File) xmlDecoder(name string) (bool, *xml.Decoder, *os.File, error) {
return true, f.xmlNewDecoder(tempFile), tempFile, err
}
// SetRowHeight provides a function to set the height of a single row. For
// example, set the height of the first row in Sheet1:
// SetRowHeight provides a function to set the height of a single row. If the
// 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)
func (f *File) SetRowHeight(sheet string, row int, height float64) error {
@ -357,6 +366,9 @@ func (f *File) SetRowHeight(sheet string, row int, height float64) error {
if height > MaxRowHeight {
return ErrMaxRowHeight
}
if height < -1 {
return ErrParameterInvalid
}
ws, err := f.workSheetReader(sheet)
if err != nil {
return err
@ -365,9 +377,14 @@ func (f *File) SetRowHeight(sheet string, row int, height float64) error {
ws.prepareSheetXML(0, row)
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].CustomHeight = true
return nil
return err
}
// getRowHeight provides a function to get row height in pixels by given sheet
@ -628,7 +645,7 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) error {
}
if row2 < 1 || row == row2 {
return nil
return err
}
var ok bool
@ -636,7 +653,7 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) error {
for i, r := range ws.SheetData.Row {
if r.R == row {
rowCopy = deepcopy.Copy(ws.SheetData.Row[i]).(xlsxRow)
deepcopy.Copy(&rowCopy, ws.SheetData.Row[i])
ok = true
break
}
@ -647,7 +664,7 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) error {
}
if !ok {
return nil
return err
}
idx2 := -1
@ -657,24 +674,106 @@ func (f *File) DuplicateRowTo(sheet string, row, row2 int) error {
break
}
}
if idx2 == -1 && len(ws.SheetData.Row) >= row2 {
return nil
}
rowCopy.C = append(make([]xlsxC, 0, len(rowCopy.C)), rowCopy.C...)
f.adjustSingleRowDimensions(&rowCopy, row2, row2-row, true)
rowCopy.adjustSingleRowDimensions(row2 - row)
_ = f.adjustSingleRowFormulas(sheet, sheet, &rowCopy, row, row2-row, true)
if idx2 != -1 {
ws.SheetData.Row[idx2] = rowCopy
} else {
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
// 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 {
return nil
}
@ -708,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
// 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="B15" s="2" />
// <c r="F15" s="1" />
@ -717,7 +816,7 @@ func (f *File) duplicateMergeCells(sheet string, ws *xlsxWorksheet, row, row2 in
//
// 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="B15" s="2" />
// <c r="C15" s="2" />

View File

@ -239,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>`)))
assert.True(t, rows.Next())
_, err = rows.Columns()
assert.EqualError(t, err, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), err)
// Test token is nil
rows.decoder = f.xmlNewDecoder(bytes.NewReader(nil))
@ -353,6 +353,15 @@ func TestRemoveRow(t *testing.T) {
assert.NoError(t, f.RemoveRow(sheet1, 10))
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
assert.EqualError(t, f.RemoveRow("SheetN", 1), "sheet SheetN does not exist")
// Test remove row with invalid sheet name
@ -870,6 +879,60 @@ func TestDuplicateRow(t *testing.T) {
f := NewFile()
// Test duplicate row with invalid sheet name
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) {
@ -896,9 +959,9 @@ func TestDuplicateMergeCells(t *testing.T) {
ws := &xlsxWorksheet{MergeCells: &xlsxMergeCells{
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"
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) {
@ -932,8 +995,7 @@ func TestGetValueFromNumber(t *testing.T) {
}
func TestErrSheetNotExistError(t *testing.T) {
err := ErrSheetNotExist{SheetName: "Sheet1"}
assert.EqualValues(t, err.Error(), "sheet Sheet1 does not exist")
assert.Equal(t, "sheet Sheet1 does not exist", ErrSheetNotExist{"Sheet1"}.Error())
}
func TestCheckRow(t *testing.T) {
@ -945,7 +1007,7 @@ func TestCheckRow(t *testing.T) {
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.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())
}
@ -981,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")
}
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) {
f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
if !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
// the LICENSE file.
//
@ -7,7 +7,7 @@
// 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.
// data. This library needs Go version 1.18 or later.
package excelize
@ -38,10 +38,10 @@ func parseShapeOptions(opts *Shape) (*Shape, error) {
opts.Format.Locked = boolPtr(false)
}
if opts.Format.ScaleX == 0 {
opts.Format.ScaleX = defaultPictureScale
opts.Format.ScaleX = defaultDrawingScale
}
if opts.Format.ScaleY == 0 {
opts.Format.ScaleY = defaultPictureScale
opts.Format.ScaleY = defaultDrawingScale
}
if opts.Line.Width == nil {
opts.Line.Width = float64Ptr(defaultShapeLineWidth)
@ -50,13 +50,13 @@ func parseShapeOptions(opts *Shape) (*Shape, error) {
}
// 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
// print settings) and properties set. For example, add text box (rect shape)
// in Sheet1:
// name and shape format set (such as offset, scale, aspect ratio setting and
// print settings). For example, add text box (rect shape) in Sheet1:
//
// lineWidth := 1.2
// err := f.AddShape("Sheet1", "G6",
// err := f.AddShape("Sheet1",
// &excelize.Shape{
// Cell: "G6",
// Type: "rect",
// Line: excelize.ShapeLine{Color: "4286F4", Width: &lineWidth},
// Fill: excelize.Fill{Color: []string{"8EB9FF"}, Pattern: 1},
@ -293,7 +293,7 @@ func (f *File) AddShape(sheet string, opts *Shape) error {
if err != nil {
return err
}
// Read sheet data.
// Read sheet data
ws, err := f.workSheetReader(sheet)
if err != nil {
return err
@ -322,29 +322,27 @@ func (f *File) AddShape(sheet string, opts *Shape) error {
return f.addContentTypePart(drawingID, "drawings")
}
// addDrawingShape provides a function to add preset geometry by given sheet,
// drawingXMLand format sets.
func (f *File) addDrawingShape(sheet, drawingXML, cell string, opts *Shape) error {
// twoCellAnchorShape create a two cell anchor shape size placeholder for a
// group, a shape, or a drawing element.
func (f *File) twoCellAnchorShape(sheet, drawingXML, cell string, width, height uint, format GraphicOptions) (*xlsxWsDr, *xdrCellAnchor, int, error) {
fromCol, fromRow, err := CellNameToCoordinates(cell)
if err != nil {
return err
return nil, nil, 0, err
}
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, fromCol, fromRow, opts.Format.OffsetX, opts.Format.OffsetY,
width, height)
w := int(float64(width) * format.ScaleX)
h := int(float64(height) * format.ScaleY)
colStart, rowStart, colEnd, rowEnd, x2, y2 := f.positionObjectPixels(sheet, fromCol, fromRow, format.OffsetX, format.OffsetY, w, h)
content, cNvPrID, err := f.drawingParser(drawingXML)
if err != nil {
return err
return content, nil, cNvPrID, err
}
twoCellAnchor := xdrCellAnchor{}
twoCellAnchor.EditAs = opts.Format.Positioning
twoCellAnchor.EditAs = format.Positioning
from := xlsxFrom{}
from.Col = colStart
from.ColOff = opts.Format.OffsetX * EMU
from.ColOff = format.OffsetX * EMU
from.Row = rowStart
from.RowOff = opts.Format.OffsetY * EMU
from.RowOff = format.OffsetY * EMU
to := xlsxTo{}
to.Col = colEnd
to.ColOff = x2 * EMU
@ -352,6 +350,17 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, opts *Shape) erro
to.RowOff = y2 * EMU
twoCellAnchor.From = &from
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
if len(opts.Fill.Color) == 1 {
solidColor = opts.Fill.Color[0]
@ -462,7 +471,7 @@ func (f *File) addDrawingShape(sheet, drawingXML, cell string, opts *Shape) erro
FLocksWithSheet: *opts.Format.Locked,
FPrintsWithSheet: *opts.Format.PrintObject,
}
content.TwoCellAnchor = append(content.TwoCellAnchor, &twoCellAnchor)
content.TwoCellAnchor = append(content.TwoCellAnchor, twoCellAnchor)
f.Drawings.Store(drawingXML, content)
return err
}

View File

@ -42,16 +42,16 @@ func TestAddShape(t *testing.T) {
},
},
), "sheet Sheet3 does not exist")
assert.EqualError(t, f.AddShape("Sheet3", nil), ErrParameterInvalid.Error())
assert.EqualError(t, f.AddShape("Sheet1", &Shape{Cell: "A1"}), ErrParameterInvalid.Error())
assert.EqualError(t, f.AddShape("Sheet1", &Shape{
assert.Equal(t, ErrParameterInvalid, f.AddShape("Sheet3", nil))
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"}},
},
}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
}))
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddShape1.xlsx")))
// Test add first shape for given sheet
@ -79,14 +79,14 @@ func TestAddShape(t *testing.T) {
}))
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddShape2.xlsx")))
// Test add shape with invalid sheet name
assert.EqualError(t, f.AddShape("Sheet:1", &Shape{
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"}},
},
}), ErrSheetNameInvalid.Error())
}))
// Test add shape with unsupported charset style sheet
f.Styles = nil
f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset)

248
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
// the LICENSE file.
//
@ -7,7 +7,7 @@
// 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.
// data. This library needs Go version 1.18 or later.
package excelize
@ -27,7 +27,7 @@ import (
"unicode/utf16"
"unicode/utf8"
"github.com/mohae/deepcopy"
"github.com/tiendc/go-deepcopy"
)
// NewSheet provides the function to create a new sheet by given a worksheet
@ -124,7 +124,8 @@ func (f *File) mergeExpandedCols(ws *xlsxWorksheet) {
Width: ws.Cols.Col[i-1].Width,
}, 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 {
column.Max = ws.Cols.Col[i-1].Min
}
@ -164,10 +165,10 @@ func (f *File) workSheetWriter() {
// reusing buffer
_ = encoder.Encode(sheet)
f.saveFileList(p.(string), replaceRelationshipsBytes(f.replaceNameSpaceBytes(p.(string), buffer.Bytes())))
ok := f.checked[p.(string)]
_, ok := f.checked.Load(p.(string))
if ok {
f.Sheet.Delete(p.(string))
f.checked[p.(string)] = false
f.checked.Delete(p.(string))
}
buffer.Reset()
}
@ -179,35 +180,38 @@ func (f *File) workSheetWriter() {
func trimRow(sheetData *xlsxSheetData) []xlsxRow {
var (
row xlsxRow
rows []xlsxRow
i int
)
for k, v := range sheetData.Row {
for k := range sheetData.Row {
row = sheetData.Row[k]
if row.C = trimCell(v.C); len(row.C) != 0 || row.hasAttr() {
rows = append(rows, row)
if row = trimCell(row); len(row.C) != 0 || row.hasAttr() {
sheetData.Row[i] = row
}
i++
}
return rows
return sheetData.Row[:i]
}
// 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
for i := range column {
rowFull = column[i].hasValue() && rowFull
}
if rowFull {
return column
return row
}
col := make([]xlsxC, len(column))
i := 0
for _, c := range column {
if c.hasValue() {
col[i] = c
row.C[i] = c
i++
}
}
return col[:i]
row.C = row.C[:i]
return row
}
// setContentTypes provides a function to read and update property of contents
@ -237,7 +241,7 @@ func (f *File) setSheet(index int, name string) {
sheetXMLPath := "xl/worksheets/sheet" + strconv.Itoa(index) + ".xml"
f.sheetMap[name] = sheetXMLPath
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
@ -360,7 +364,7 @@ func (f *File) SetSheetName(source, target string) error {
if err = checkSheetName(target); err != nil {
return err
}
if strings.EqualFold(target, source) {
if target == source {
return err
}
wb, _ := f.workbookReader()
@ -371,6 +375,12 @@ func (f *File) SetSheetName(source, target string) error {
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
}
@ -576,14 +586,14 @@ func (f *File) DeleteSheet(sheet string) error {
}
}
target := f.deleteSheetFromWorkbookRels(v.ID)
_ = f.deleteSheetFromContentTypes(target)
_ = f.removeContentTypesPart(ContentTypeSpreadSheetMLWorksheet, target)
_ = f.deleteCalcChain(f.getSheetID(sheet), "")
delete(f.sheetMap, v.Name)
f.Pkg.Delete(sheetXML)
f.Pkg.Delete(rels)
f.Relationships.Delete(rels)
f.Sheet.Delete(sheetXML)
delete(f.xmlAttr, sheetXML)
f.xmlAttr.Delete(sheetXML)
f.SheetCount--
}
index, err := f.GetSheetIndex(activeSheetName)
@ -591,6 +601,49 @@ func (f *File) DeleteSheet(sheet string) error {
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
// by given worksheet ID.
func deleteAndAdjustDefinedNames(wb *xlsxWorkbook, deleteLocalSheetID int) {
@ -626,24 +679,50 @@ func (f *File) deleteSheetFromWorkbookRels(rID string) string {
return ""
}
// deleteSheetFromContentTypes provides a function to remove worksheet
// relationships by given target name in the file [Content_Types].xml.
func (f *File) deleteSheetFromContentTypes(target string) error {
if !strings.HasPrefix(target, "/") {
target = "/xl/" + target
// 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"
}
content, err := f.contentTypesReader()
if err != nil {
return err
rels := "xl/worksheets/_rels/" + strings.TrimPrefix(name, "xl/worksheets/") + ".rels"
sheetRels, _ := f.relsReader(rels)
if sheetRels == nil {
sheetRels = &xlsxRelationships{}
}
content.mu.Lock()
defer content.mu.Unlock()
for k, v := range content.Overrides {
if v.PartName == target {
content.Overrides = append(content.Overrides[:k], content.Overrides[k+1:]...)
sheetRels.mu.Lock()
defer sheetRels.mu.Unlock()
for k, v := range sheetRels.Relationships {
if v.ID == rID {
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
@ -672,7 +751,8 @@ func (f *File) copySheet(from, to int) error {
if err != nil {
return err
}
worksheet := deepcopy.Copy(sheet).(*xlsxWorksheet)
worksheet := &xlsxWorksheet{}
deepcopy.Copy(worksheet, sheet)
toSheetID := strconv.Itoa(f.getSheetID(f.GetSheetName(to)))
sheetXMLPath := "xl/worksheets/sheet" + toSheetID + ".xml"
if len(worksheet.SheetViews.SheetView) > 0 {
@ -688,8 +768,8 @@ func (f *File) copySheet(from, to int) error {
f.Pkg.Store(toRels, rels.([]byte))
}
fromSheetXMLPath, _ := f.getSheetXMLPath(fromSheet)
fromSheetAttr := f.xmlAttr[fromSheetXMLPath]
f.xmlAttr[sheetXMLPath] = fromSheetAttr
fromSheetAttr, _ := f.xmlAttr.Load(fromSheetXMLPath)
f.xmlAttr.Store(sheetXMLPath, fromSheetAttr)
return err
}
@ -738,6 +818,11 @@ func (f *File) SetSheetVisible(sheet string, visible bool, veryHidden ...bool) e
return err
}
tabSelected := false
if ws.SheetViews == nil {
ws.SheetViews = &xlsxSheetViews{
SheetView: []xlsxSheetView{{WorkbookViewID: 0}},
}
}
if len(ws.SheetViews.SheetView) > 0 {
tabSelected = ws.SheetViews.SheetView[0].TabSelected
}
@ -1123,8 +1208,8 @@ func attrValToBool(name string, attrs []xml.Attr) (val bool, err error) {
// DifferentFirst | Different first-page header and footer indicator
// DifferentOddEven | Different odd and even page headers and footers indicator
// ScaleWithDoc | Scale header and footer with document scaling
// OddFooter | Odd Page Footer
// OddHeader | Odd Header
// OddFooter | Odd Page Footer, or primary Page Footer if 'DifferentOddEven' is 'false'
// OddHeader | Odd Header, or primary Page Header if 'DifferentOddEven' is 'false'
// EvenFooter | Even Page Footer
// EvenHeader | Even Page Header
// FirstFooter | First Page Footer
@ -1156,7 +1241,7 @@ func attrValToBool(name string, attrs []xml.Attr) (val bool, err error) {
// |
// &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
// |
@ -1260,6 +1345,32 @@ func (f *File) SetHeaderFooter(sheet string, opts *HeaderFooterOptions) error {
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
// deliberately changing, moving, or deleting data in a worksheet. The
// optional field AlgorithmName specified hash algorithm, support XOR, MD4,
@ -1500,8 +1611,7 @@ func (f *File) SetPageLayout(sheet string, opts *PageLayoutOptions) error {
if opts == nil {
return err
}
ws.setPageSetUp(opts)
return err
return ws.setPageSetUp(opts)
}
// newPageSetUp initialize page setup settings for the worksheet if which not
@ -1513,12 +1623,15 @@ func (ws *xlsxWorksheet) newPageSetUp() {
}
// 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 {
ws.newPageSetUp()
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.PageSetUp.Orientation = *opts.Orientation
}
@ -1527,7 +1640,10 @@ func (ws *xlsxWorksheet) setPageSetUp(opts *PageLayoutOptions) {
ws.PageSetUp.FirstPageNumber = strconv.Itoa(int(*opts.FirstPageNumber))
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.PageSetUp.Scale = int(*opts.AdjustTo)
}
@ -1543,13 +1659,21 @@ func (ws *xlsxWorksheet) setPageSetUp(opts *PageLayoutOptions) {
ws.newPageSetUp()
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.
func (f *File) GetPageLayout(sheet string) (PageLayoutOptions, error) {
opts := PageLayoutOptions{
Size: intPtr(0),
Orientation: stringPtr("portrait"),
Orientation: stringPtr(supportedPageOrientation[0]),
FirstPageNumber: uintPtr(1),
AdjustTo: uintPtr(100),
}
@ -1577,6 +1701,9 @@ func (f *File) GetPageLayout(sheet string) (PageLayoutOptions, error) {
opts.FitToWidth = ws.PageSetUp.FitToWidth
}
opts.BlackAndWhite = boolPtr(ws.PageSetUp.BlackAndWhite)
if ws.PageSetUp.PageOrder != "" {
opts.PageOrder = stringPtr(ws.PageSetUp.PageOrder)
}
}
return opts, err
}
@ -1591,11 +1718,29 @@ func (f *File) GetPageLayout(sheet string) (PageLayoutOptions, error) {
// Comment: "defined name comment",
// 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 {
if definedName.Name == "" || definedName.RefersTo == "" {
return ErrParameterInvalid
}
if err := checkDefinedName(definedName.Name); err != nil {
if err := checkDefinedName(definedName.Name); err != nil && inStrSlice(builtInDefinedNames[:2], definedName.Name, false) == -1 {
return err
}
wb, err := f.workbookReader()
@ -1713,12 +1858,9 @@ func (f *File) GroupSheets(sheets []string) error {
}
for _, ws := range wss {
sheetViews := ws.SheetViews.SheetView
if len(sheetViews) > 0 {
for idx := range sheetViews {
ws.SheetViews.SheetView[idx].TabSelected = true
}
continue
}
}
return nil
}
@ -1732,12 +1874,10 @@ func (f *File) UngroupSheets() error {
}
ws, _ := f.workSheetReader(sheet)
sheetViews := ws.SheetViews.SheetView
if len(sheetViews) > 0 {
for idx := range sheetViews {
ws.SheetViews.SheetView[idx].TabSelected = false
}
}
}
return nil
}
@ -1864,7 +2004,7 @@ func (f *File) RemovePageBreak(sheet, cell string) error {
}
// 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) {
rels, _ := f.Relationships.Load(path)
if rels == nil {
@ -1958,7 +2098,7 @@ func (f *File) SetSheetDimension(sheet string, rangeRef string) error {
return err
}
_ = sortCoordinates(coordinates)
ref, err := f.coordinatesToRangeRef(coordinates)
ref, err := coordinatesToRangeRef(coordinates)
ws.Dimension = &xlsxDimension{Ref: ref}
return err
}

View File

@ -8,6 +8,7 @@ import (
"path/filepath"
"strconv"
"strings"
"sync"
"testing"
"github.com/stretchr/testify/assert"
@ -120,7 +121,7 @@ func TestPanes(t *testing.T) {
// Test add pane on empty sheet views worksheet
f = NewFile()
f.checked = nil
f.checked = sync.Map{}
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>`))
assert.NoError(t, f.SetPanes("Sheet1",
@ -173,19 +174,19 @@ func TestSearchSheet(t *testing.T) {
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>A</t></is></c></row></sheetData></worksheet>`))
f.checked = nil
f.checked = sync.Map{}
result, err = f.SearchSheet("Sheet1", "A")
assert.EqualError(t, err, "strconv.Atoi: parsing \"A\": invalid syntax")
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>`))
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)
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")
assert.EqualError(t, err, "invalid cell reference [1, 0]")
assert.Equal(t, newCoordinatesToCellNameError(1, 0), err)
assert.Equal(t, []string(nil), result)
// Test search sheet with unsupported charset shared strings table
@ -209,6 +210,7 @@ func TestSetPageLayout(t *testing.T) {
FitToHeight: intPtr(2),
FitToWidth: intPtr(2),
BlackAndWhite: boolPtr(true),
PageOrder: stringPtr("overThenDown"),
}
assert.NoError(t, f.SetPageLayout("Sheet1", &expected))
opts, err := f.GetPageLayout("Sheet1")
@ -218,6 +220,16 @@ func TestSetPageLayout(t *testing.T) {
assert.EqualError(t, f.SetPageLayout("SheetN", nil), "sheet SheetN does not exist")
// Test set page layout with invalid sheet name
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) {
@ -230,8 +242,16 @@ func TestGetPageLayout(t *testing.T) {
assert.EqualError(t, err, ErrSheetNameInvalid.Error())
}
func TestSetHeaderFooter(t *testing.T) {
func TestHeaderFooter(t *testing.T) {
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"))
// Test set header and footer on not exists worksheet
assert.EqualError(t, f.SetHeaderFooter("SheetN", nil), "sheet SheetN does not exist")
@ -251,7 +271,7 @@ func TestSetHeaderFooter(t *testing.T) {
EvenFooter: text,
FirstHeader: text,
}))
assert.NoError(t, f.SetHeaderFooter("Sheet1", &HeaderFooterOptions{
expected := &HeaderFooterOptions{
DifferentFirst: true,
DifferentOddEven: true,
OddHeader: "&R&P",
@ -259,14 +279,18 @@ func TestSetHeaderFooter(t *testing.T) {
EvenHeader: "&L&P",
EvenFooter: "&L&D&R&T",
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")))
}
func TestDefinedName(t *testing.T) {
f := NewFile()
assert.NoError(t, f.SetDefinedName(&DefinedName{
Name: "Amount",
Name: "Amount.",
RefersTo: "Sheet1!$A$2:$D$5",
Comment: "defined name comment",
Scope: "Sheet1",
@ -276,6 +300,16 @@ func TestDefinedName(t *testing.T) {
RefersTo: "Sheet1!$A$2:$D$5",
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{
Name: "Amount",
RefersTo: "Sheet1!$A$2:$D$5",
@ -297,7 +331,7 @@ func TestDefinedName(t *testing.T) {
Name: "Amount",
}))
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")))
// Test set defined name with unsupported charset workbook
f.WorkBook = nil
@ -442,8 +476,30 @@ func TestSetSheetName(t *testing.T) {
// Test set worksheet with the same name
assert.NoError(t, f.SetSheetName("Sheet1", "Sheet1"))
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
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) {
@ -452,7 +508,7 @@ func TestWorksheetWriter(t *testing.T) {
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>`
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))
f.workSheetWriter()
value, ok := f.Pkg.Load("xl/worksheets/sheet1.xml")
@ -502,6 +558,43 @@ func TestDeleteSheet(t *testing.T) {
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) {
deleteAndAdjustDefinedNames(nil, 0)
deleteAndAdjustDefinedNames(&xlsxWorkbook{}, 0)
@ -525,6 +618,18 @@ func TestSetSheetVisible(t *testing.T) {
f.WorkBook = nil
f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset)
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) {
@ -551,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")
}
func TestDeleteSheetFromContentTypes(t *testing.T) {
func TestRemoveContentTypesPart(t *testing.T) {
f := NewFile()
// Test delete sheet from content types with unsupported charset content types
f.ContentTypes = nil
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) {

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
// the LICENSE file.
//
@ -7,7 +7,7 @@
// 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.
// data. This library needs Go version 1.18 or later.
package excelize

View File

@ -30,7 +30,7 @@ func TestSetPageMargins(t *testing.T) {
// Test set page margins on not exists worksheet
assert.EqualError(t, f.SetPageMargins("SheetN", nil), "sheet SheetN does not exist")
// 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) {
@ -40,7 +40,7 @@ func TestGetPageMargins(t *testing.T) {
assert.EqualError(t, err, "sheet SheetN does not exist")
// Test get page margins with invalid sheet name
_, err = f.GetPageMargins("Sheet:1")
assert.EqualError(t, err, ErrSheetNameInvalid.Error())
assert.Equal(t, ErrSheetNameInvalid, err)
}
func TestSetSheetProps(t *testing.T) {
@ -88,7 +88,7 @@ func TestSetSheetProps(t *testing.T) {
// Test set worksheet properties on not exists worksheet
assert.EqualError(t, f.SetSheetProps("SheetN", nil), "sheet SheetN does not exist")
// 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) {
@ -98,5 +98,5 @@ func TestGetSheetProps(t *testing.T) {
assert.EqualError(t, err, "sheet SheetN does not exist")
// Test get worksheet properties with invalid sheet name
_, 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
// the LICENSE file.
//
@ -7,7 +7,7 @@
// 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.
// data. This library needs Go version 1.18 or later.
package excelize

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
// the LICENSE file.
//
@ -7,7 +7,7 @@
// 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.
// data. This library needs Go version 1.18 or later.
package excelize
@ -18,10 +18,10 @@ import (
"strings"
)
// addSparklineGroupByStyle provides a function to create x14:sparklineGroups
// element by given sparkline style ID.
func (f *File) addSparklineGroupByStyle(ID int) *xlsxX14SparklineGroup {
groups := []*xlsxX14SparklineGroup{
// getSparklineGroupPresets returns the preset list of sparkline group to create
// x14:sparklineGroups element.
func getSparklineGroupPresets() []*xlsxX14SparklineGroup {
return []*xlsxX14SparklineGroup{
{
ColorSeries: &xlsxColor{Theme: intPtr(4), Tint: -0.499984740745262},
ColorNegative: &xlsxColor{Theme: intPtr(5)},
@ -356,15 +356,14 @@ func (f *File) addSparklineGroupByStyle(ID int) *xlsxX14SparklineGroup {
ColorLow: &xlsxColor{Theme: intPtr(7)},
}, // 36
}
return groups[ID]
}
// AddSparkline provides a function to add sparklines to the worksheet by
// 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
// 2010 and later only. You can write them to an XLSX file that can be read by
// Excel 2007, but they won't be displayed. For example, add a grouped
// sparkline. Changes are applied to all three:
// 2010 and later only. You can write them to workbook that can be read by Excel
// 2007, but they won't be displayed. For example, add a grouped sparkline.
// Changes are applied to all three:
//
// err := f.AddSparkline("Sheet1", &excelize.SparklineOptions{
// Location: []string{"A1", "A2", "A3"},
@ -415,7 +414,7 @@ func (f *File) AddSparkline(sheet string, opts *SparklineOptions) error {
}
sparkType = specifiedSparkTypes
}
group = f.addSparklineGroupByStyle(opts.Style)
group = getSparklineGroupPresets()[opts.Style]
group.Type = sparkType
group.ColorAxis = &xlsxColor{RGB: "FF000000"}
group.DisplayEmptyCellsAs = "gap"
@ -489,9 +488,9 @@ func (f *File) appendSparkline(ws *xlsxWorksheet, group *xlsxX14SparklineGroup,
err error
idx int
appendMode bool
decodeExtLst = new(decodeWorksheetExt)
decodeExtLst = new(decodeExtLst)
decodeSparklineGroups *decodeX14SparklineGroups
ext *xlsxWorksheetExt
ext *xlsxExt
sparklineGroupsBytes, sparklineGroupBytes, extLstBytes []byte
)
sparklineGroupBytes, _ = xml.Marshal(group)
@ -523,13 +522,13 @@ func (f *File) appendSparkline(ws *xlsxWorksheet, group *xlsxX14SparklineGroup,
XMLNSXM: NameSpaceSpreadSheetExcel2006Main.Value,
SparklineGroups: []*xlsxX14SparklineGroup{group},
})
decodeExtLst.Ext = append(decodeExtLst.Ext, &xlsxWorksheetExt{
decodeExtLst.Ext = append(decodeExtLst.Ext, &xlsxExt{
URI: ExtURISparklineGroups, Content: string(sparklineGroupsBytes),
})
}
sort.Slice(decodeExtLst.Ext, func(i, j int) bool {
return inStrSlice(extensionURIPriority, decodeExtLst.Ext[i].URI, false) <
inStrSlice(extensionURIPriority, decodeExtLst.Ext[j].URI, false)
return inStrSlice(worksheetExtURIPriority, decodeExtLst.Ext[i].URI, false) <
inStrSlice(worksheetExtURIPriority, decodeExtLst.Ext[j].URI, false)
})
extLstBytes, err = xml.Marshal(decodeExtLst)
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"},
}), "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
assert.EqualError(t, f.AddSparkline("Sheet:1", &SparklineOptions{
assert.Equal(t, ErrSheetNameInvalid, f.AddSparkline("Sheet:1", &SparklineOptions{
Location: []string{"F3"},
Range: []string{"Sheet2!A3:E3"},
Type: "win_loss",
Negative: true,
}), ErrSheetNameInvalid.Error())
}))
assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOptions{
assert.Equal(t, ErrSparklineLocation, f.AddSparkline("Sheet1", &SparklineOptions{
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"},
}), ErrSparklineRange.Error())
}))
assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOptions{
assert.Equal(t, ErrSparkline, f.AddSparkline("Sheet1", &SparklineOptions{
Location: []string{"F2", "F3"},
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"},
Range: []string{"Sheet2!A3:E3"},
Type: "unknown_type",
}), ErrSparklineType.Error())
}))
assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOptions{
assert.Equal(t, ErrSparklineStyle, f.AddSparkline("Sheet1", &SparklineOptions{
Location: []string{"F3"},
Range: []string{"Sheet2!A3:E3"},
Style: -1,
}), ErrSparklineStyle.Error())
}))
assert.EqualError(t, f.AddSparkline("Sheet1", &SparklineOptions{
assert.Equal(t, ErrSparklineStyle, f.AddSparkline("Sheet1", &SparklineOptions{
Location: []string{"F3"},
Range: []string{"Sheet2!A3:E3"},
Style: -1,
}), ErrSparklineStyle.Error())
}))
// Test creating a conditional format with existing extension lists
ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
assert.True(t, ok)
ws.(*xlsxWorksheet).ExtLst = &xlsxExtLst{Ext: `
<ext uri="{A8765BA9-456A-4dab-B4F3-ACF838C121DE}"><x14:slicerList /></ext>
<ext uri="{05C60535-1F16-4fd2-B633-F4F36F0B64E0}"><x14:sparklineGroups /></ext>`}
ws.(*xlsxWorksheet).ExtLst = &xlsxExtLst{Ext: fmt.Sprintf(`<ext uri="%s"><x14:slicerList /></ext><ext uri="%s"><x14:sparklineGroups /></ext>`, ExtURISlicerListX14, ExtURISparklineGroups)}
assert.NoError(t, f.AddSparkline("Sheet1", &SparklineOptions{
Location: []string{"A3"},
Range: []string{"Sheet3!A2:J2"},
Type: "column",
}))
// 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{
Location: []string{"A2"},
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
// the LICENSE file.
//
@ -7,7 +7,7 @@
// 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.
// data. This library needs Go version 1.18 or later.
package excelize
@ -38,14 +38,16 @@ type StreamWriter struct {
tableParts string
}
// NewStreamWriter return stream writer struct by given worksheet name for
// generate new worksheet with large amounts of data. Note that after set
// rows, you must call the 'Flush' method to end the streaming writing process
// and ensure that the order of row numbers is ascending, the normal mode
// functions and stream mode functions can't be work mixed to writing data on
// the worksheets, you can't get cell value when in-memory chunks data over
// 16MB. For example, set data for worksheet of size 102400 rows x 50 columns
// with numbers and style:
// NewStreamWriter returns stream writer struct by given worksheet name used for
// writing data on a new existing empty worksheet with large amounts of data.
// Note that after writing data with the stream writer for the worksheet, you
// must call the 'Flush' method to end the streaming writing process, ensure
// that the order of row numbers is ascending when set rows, and the normal
// mode functions and stream mode functions can not be work mixed to writing
// data on the worksheets. The stream writer will try to use temporary files on
// 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()
// defer func() {
@ -116,7 +118,7 @@ func (f *File) NewStreamWriter(sheet string) (*StreamWriter, error) {
}
sheetID := f.getSheetID(sheet)
if sheetID == -1 {
return nil, newNoExistSheetError(sheet)
return nil, ErrSheetNotExist{sheet}
}
sw := &StreamWriter{
file: f,
@ -182,7 +184,7 @@ func (sw *StreamWriter) AddTable(table *Table) error {
}
// Correct table reference range, such correct C1:B3 to B1:C3.
ref, err := sw.file.coordinatesToRangeRef(coordinates)
ref, err := coordinatesToRangeRef(coordinates)
if err != nil {
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) {
startElement, ok = token.(xml.StartElement)
if !ok {
@ -437,24 +439,24 @@ func (sw *StreamWriter) SetRow(cell string, values []interface{}, opts ...RowOpt
// the width column B:C as 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 {
return ErrStreamSetColWidth
}
if min < MinColumns || min > MaxColumns || max < MinColumns || max > MaxColumns {
if minVal < MinColumns || minVal > MaxColumns || maxVal < MinColumns || maxVal > MaxColumns {
return ErrColumnNumber
}
if width > MaxColumnWidth {
return ErrColumnWidth
}
if min > max {
min, max = max, min
if minVal > maxVal {
minVal, maxVal = maxVal, minVal
}
sw.cols.WriteString(`<col min="`)
sw.cols.WriteString(strconv.Itoa(min))
sw.cols.WriteString(strconv.Itoa(minVal))
sw.cols.WriteString(`" max="`)
sw.cols.WriteString(strconv.Itoa(max))
sw.cols.WriteString(strconv.Itoa(maxVal))
sw.cols.WriteString(`" width="`)
sw.cols.WriteString(strconv.FormatFloat(width, 'f', -1, 64))
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
// the StreamWriter. Don't create a merged cell that overlaps with another
// existing merged cell.
func (sw *StreamWriter) MergeCell(hCell, vCell string) error {
_, err := cellRefsToCoordinates(hCell, vCell)
func (sw *StreamWriter) MergeCell(topLeftCell, bottomRightCell string) error {
_, err := cellRefsToCoordinates(topLeftCell, bottomRightCell)
if err != nil {
return err
}
sw.mergeCellsCount++
_, _ = sw.mergeCells.WriteString(`<mergeCell ref="`)
_, _ = sw.mergeCells.WriteString(hCell)
_, _ = sw.mergeCells.WriteString(topLeftCell)
_, _ = sw.mergeCells.WriteString(`:`)
_, _ = sw.mergeCells.WriteString(vCell)
_, _ = sw.mergeCells.WriteString(bottomRightCell)
_, _ = sw.mergeCells.WriteString(`"/>`)
return nil
}
@ -525,11 +527,11 @@ func (sw *StreamWriter) setCellValFunc(c *xlsxC, val interface{}) error {
var err error
switch val := val.(type) {
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
err = setCellIntFunc(c, val)
setCellIntFunc(c, val)
case float32:
c.T, c.V = setCellFloat(float64(val), -1, 32)
c.setCellFloat(float64(val), -1, 32)
case float64:
c.T, c.V = setCellFloat(val, -1, 64)
c.setCellFloat(val, -1, 64)
case string:
c.setCellValue(val)
case []byte:
@ -552,7 +554,7 @@ func (sw *StreamWriter) setCellValFunc(c *xlsxC, val interface{}) error {
}
// setCellIntFunc is a wrapper of SetCellInt.
func setCellIntFunc(c *xlsxC, val interface{}) (err error) {
func setCellIntFunc(c *xlsxC, val interface{}) {
switch val := val.(type) {
case int:
c.T, c.V = setCellInt(val)
@ -565,18 +567,16 @@ func setCellIntFunc(c *xlsxC, val interface{}) (err error) {
case int64:
c.T, c.V = setCellInt(int(val))
case uint:
c.T, c.V = setCellInt(int(val))
c.T, c.V = setCellUint(uint64(val))
case uint8:
c.T, c.V = setCellInt(int(val))
c.T, c.V = setCellUint(uint64(val))
case uint16:
c.T, c.V = setCellInt(int(val))
c.T, c.V = setCellUint(uint64(val))
case uint32:
c.T, c.V = setCellInt(int(val))
c.T, c.V = setCellUint(uint64(val))
case uint64:
c.T, c.V = setCellInt(int(val))
default:
c.T, c.V = setCellUint(val)
}
return
}
// 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]
sw.file.Sheet.Delete(sheetPath)
delete(sw.file.checked, sheetPath)
sw.file.checked.Delete(sheetPath)
sw.file.Pkg.Delete(sheetPath)
return nil

View File

@ -3,6 +3,8 @@ package excelize
import (
"encoding/xml"
"fmt"
"io"
"math"
"math/rand"
"os"
"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("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++ {
row := make([]interface{}, 50)
@ -144,7 +148,7 @@ func TestStreamWriter(t *testing.T) {
cells += len(row)
}
assert.NoError(t, rows.Close())
assert.Equal(t, 2559559, cells)
assert.Equal(t, 2559562, cells)
// Save spreadsheet with password.
assert.NoError(t, file.SaveAs(filepath.Join("test", "EncryptionTestStreamWriter.xlsx"), Options{Password: "password"}))
assert.NoError(t, file.Close())
@ -158,11 +162,11 @@ func TestStreamSetColWidth(t *testing.T) {
streamWriter, err := file.NewStreamWriter("Sheet1")
assert.NoError(t, err)
assert.NoError(t, streamWriter.SetColWidth(3, 2, 20))
assert.ErrorIs(t, streamWriter.SetColWidth(0, 3, 20), ErrColumnNumber)
assert.ErrorIs(t, streamWriter.SetColWidth(MaxColumns+1, 3, 20), ErrColumnNumber)
assert.EqualError(t, streamWriter.SetColWidth(1, 3, MaxColumnWidth+1), ErrColumnWidth.Error())
assert.Equal(t, ErrColumnNumber, streamWriter.SetColWidth(0, 3, 20))
assert.Equal(t, ErrColumnNumber, streamWriter.SetColWidth(MaxColumns+1, 3, 20))
assert.Equal(t, ErrColumnWidth, streamWriter.SetColWidth(1, 3, MaxColumnWidth+1))
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) {
@ -183,9 +187,9 @@ func TestStreamSetPanes(t *testing.T) {
streamWriter, err := file.NewStreamWriter("Sheet1")
assert.NoError(t, err)
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.ErrorIs(t, streamWriter.SetPanes(paneOpts), ErrStreamSetPanes)
assert.Equal(t, ErrStreamSetPanes, streamWriter.SetPanes(paneOpts))
}
func TestStreamTable(t *testing.T) {
@ -220,10 +224,12 @@ func TestStreamTable(t *testing.T) {
assert.NoError(t, streamWriter.AddTable(&Table{Range: "A1:C1"}))
// Test add table with illegal cell reference
assert.EqualError(t, streamWriter.AddTable(&Table{Range: "A:B1"}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
assert.EqualError(t, streamWriter.AddTable(&Table{Range: "A1:B"}), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")).Error())
assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), streamWriter.AddTable(&Table{Range: "A:B1"}))
assert.Equal(t, newCellNameToCoordinatesError("B", newInvalidCellNameError("B")), streamWriter.AddTable(&Table{Range: "A1:B"}))
// Test add table with invalid table name
assert.EqualError(t, streamWriter.AddTable(&Table{Range: "A:B1", Name: "1Table"}), newInvalidNameError("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
file.ContentTypes = nil
file.Pkg.Store(defaultXMLPathContentTypes, MacintoshCyrillicCharset)
@ -239,7 +245,7 @@ func TestStreamMergeCells(t *testing.T) {
assert.NoError(t, err)
assert.NoError(t, streamWriter.MergeCell("A1", "D1"))
// 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())
// Save spreadsheet by the given path
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")
// Test new stream write with invalid sheet name
_, err = file.NewStreamWriter("Sheet:1")
assert.EqualError(t, err, ErrSheetNameInvalid.Error())
assert.Equal(t, ErrSheetNameInvalid, err)
}
func TestStreamMarshalAttrs(t *testing.T) {
@ -288,10 +294,10 @@ func TestStreamSetRow(t *testing.T) {
}()
streamWriter, err := file.NewStreamWriter("Sheet1")
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
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
file.WorkBook = nil
file.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset)
@ -332,8 +338,7 @@ func TestStreamSetRowWithStyle(t *testing.T) {
Cell{StyleID: blueStyleID, Value: "value3"},
&Cell{StyleID: blueStyleID, Value: "value3"},
}, RowOpts{StyleID: grayStyleID}))
err = streamWriter.Flush()
assert.NoError(t, err)
assert.NoError(t, streamWriter.Flush())
ws, err := file.workSheetReader("Sheet1")
assert.NoError(t, err)
@ -398,3 +403,54 @@ func TestStreamWriterOutlineLevel(t *testing.T) {
}
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)
}
}

1411
styles.go

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,7 @@
package excelize
import (
"fmt"
"math"
"path/filepath"
"strings"
@ -171,18 +172,20 @@ func TestSetConditionalFormat(t *testing.T) {
// Test creating a conditional format with a solid color data bar style
f := NewFile()
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"} {
assert.NoError(t, f.SetConditionalFormat("Sheet1", ref, condFmts))
}
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
ws, ok := f.Sheet.Load("xl/worksheets/sheet1.xml")
assert.True(t, ok)
ws.(*xlsxWorksheet).ExtLst = &xlsxExtLst{Ext: `
<ext uri="{A8765BA9-456A-4dab-B4F3-ACF838C121DE}"><x14:slicerList /></ext>
<ext uri="{05C60535-1F16-4fd2-B633-F4F36F0B64E0}"><x14:sparklineGroups /></ext>`}
ws.(*xlsxWorksheet).ExtLst = &xlsxExtLst{Ext: fmt.Sprintf(`<ext uri="%s"><x14:slicerList /></ext><ext uri="%s"><x14:sparklineGroups /></ext>`, ExtURISlicerListX14, ExtURISparklineGroups)}
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()
// 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>"}
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
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) {
for _, format := range [][]ConditionalFormatOptions{
{{Type: "cell", Format: 1, Criteria: "greater than", Value: "6"}},
{{Type: "cell", Format: 1, Criteria: "between", MinValue: "6", MaxValue: "8"}},
{{Type: "top", Format: 1, Criteria: "=", Value: "6"}},
{{Type: "bottom", Format: 1, Criteria: "=", Value: "6"}},
{{Type: "average", AboveAverage: true, Format: 1, Criteria: "="}},
{{Type: "duplicate", Format: 1, Criteria: "="}},
{{Type: "unique", Format: 1, Criteria: "="}},
{{Type: "cell", Format: intPtr(1), Criteria: "greater than", Value: "6"}},
{{Type: "cell", Format: intPtr(1), Criteria: "between", MinValue: "6", MaxValue: "8"}},
{{Type: "time_period", Format: intPtr(1), Criteria: "yesterday"}},
{{Type: "time_period", Format: intPtr(1), Criteria: "today"}},
{{Type: "time_period", Format: intPtr(1), Criteria: "tomorrow"}},
{{Type: "time_period", Format: intPtr(1), Criteria: "last 7 days"}},
{{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: "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: "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}},
} {
f := NewFile()
err := f.SetConditionalFormat("Sheet1", "A1:A2", format)
err := f.SetConditionalFormat("Sheet1", "A2:A1,B:B,2:2", format)
assert.NoError(t, err)
opts, err := f.GetConditionalFormats("Sheet1")
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()
_, 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")
// Test get conditional formats with invalid sheet name
_, err = f.GetConditionalFormats("Sheet:1")
assert.EqualError(t, err, ErrSheetNameInvalid.Error())
assert.Equal(t, ErrSheetNameInvalid, err)
}
func TestUnsetConditionalFormat(t *testing.T) {
@ -232,12 +309,12 @@ func TestUnsetConditionalFormat(t *testing.T) {
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}})
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"))
// Test unset conditional format on not exists worksheet
assert.EqualError(t, f.UnsetConditionalFormat("SheetN", "A1:A10"), "sheet SheetN does not exist")
// 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
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestUnsetConditionalFormat.xlsx")))
}
@ -264,13 +341,22 @@ func TestNewStyle(t *testing.T) {
_, err = f.NewStyle(nil)
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
f = NewFile()
_, 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)}})
assert.EqualError(t, err, ErrFontLength.Error())
assert.Equal(t, ErrFontLength, err)
_, 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
numFmt := "####;####"
@ -279,7 +365,7 @@ func TestNewStyle(t *testing.T) {
CustomNumFmt: &numFmt,
})
assert.NoError(t, err)
assert.Equal(t, 2, styleID)
assert.Equal(t, 1, styleID)
assert.NotNil(t, f.Styles)
assert.NotNil(t, f.Styles.CellXfs)
@ -294,7 +380,7 @@ func TestNewStyle(t *testing.T) {
NumFmt: 32, // must not be in currencyNumFmt
})
assert.NoError(t, err)
assert.Equal(t, 3, styleID)
assert.Equal(t, 2, styleID)
assert.NotNil(t, f.Styles)
assert.NotNil(t, f.Styles.CellXfs)
@ -353,13 +439,58 @@ func TestNewStyle(t *testing.T) {
assert.Equal(t, ErrCellStyles, err)
}
func TestNewConditionalStyle(t *testing.T) {
func TestConditionalStyle(t *testing.T) {
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
f.Styles = nil
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")
// 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) {
@ -405,7 +536,7 @@ func TestThemeReader(t *testing.T) {
f.Pkg.Store(defaultXMLPathTheme, MacintoshCyrillicCharset)
theme, err := f.themeReader()
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) {
@ -413,9 +544,9 @@ func TestSetCellStyle(t *testing.T) {
// Test set cell style on not exists worksheet
assert.EqualError(t, f.SetCellStyle("SheetN", "A1", "A2", 1), "sheet SheetN does not exist")
// 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
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
f.Styles = nil
f.Pkg.Store(defaultXMLPathStyles, MacintoshCyrillicCharset)
@ -472,3 +603,161 @@ func TestGetNumFmtID(t *testing.T) {
assert.NotEqual(t, id1, id2)
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")
}

200
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
// the LICENSE file.
//
@ -7,7 +7,7 @@
// 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.
// data. This library needs Go version 1.18 or later.
package excelize
@ -19,7 +19,6 @@ import (
"regexp"
"strconv"
"strings"
"unicode"
"unicode/utf8"
)
@ -125,13 +124,125 @@ func (f *File) AddTable(sheet string, table *Table) error {
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
// folder xl/tables.
func (f *File) countTables() int {
count := 0
f.Pkg.Range(func(k, v interface{}) bool {
if strings.Contains(k.(string), "xl/tables/table") {
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") {
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
})
@ -156,37 +267,58 @@ func (f *File) addSheetTable(sheet string, rID int) error {
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.
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 (
tableColumns []*xlsxTableColumn
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++ {
idx++
cell, err := CoordinatesToCellName(i, y1)
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 showHeaderRow {
_ = f.SetCellStr(sheet, cell, name)
}
}
if name == "" {
if name == "" || inStrSlice(header, name, true) != -1 {
name = "Column" + strconv.Itoa(idx)
if showHeaderRow {
_ = 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{
ID: idx,
Name: name,
})
}
return tableColumns, nil
tbl.TableColumns = &xlsxTableColumns{
Count: len(tableColumns),
TableColumn: tableColumns,
}
return nil
}
// checkDefinedName check whether there are illegal characters in the defined
@ -197,14 +329,22 @@ func checkDefinedName(name string) error {
if utf8.RuneCountInString(name) > MaxFieldLength {
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 {
if string(c) == "_" {
if i == 0 {
if inCodeRange(int(c), supportedDefinedNameAtStartCharCodeRange) {
continue
}
if unicode.IsLetter(c) {
continue
return newInvalidNameError(name)
}
if i > 0 && unicode.IsDigit(c) {
if inCodeRange(int(c), supportedDefinedNameAfterStartCharCodeRange) {
continue
}
return newInvalidNameError(name)
@ -224,11 +364,10 @@ func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, opts *Tab
y1++
}
// 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 {
return err
}
tableColumns, _ := f.setTableHeader(sheet, !hideHeaderRow, x1, y1, x2)
name := opts.Name
if name == "" {
name = "Table" + strconv.Itoa(i)
@ -242,10 +381,6 @@ func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, opts *Tab
AutoFilter: &xlsxAutoFilter{
Ref: ref,
},
TableColumns: &xlsxTableColumns{
Count: len(tableColumns),
TableColumn: tableColumns,
},
TableStyleInfo: &xlsxTableStyleInfo{
Name: opts.StyleName,
ShowFirstColumn: opts.ShowFirstColumn,
@ -254,13 +389,14 @@ func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, opts *Tab
ShowColumnStripes: opts.ShowColumnStripes,
},
}
_ = f.setTableColumns(sheet, !hideHeaderRow, x1, y1, x2, &t)
if hideHeaderRow {
t.AutoFilter = nil
t.HeaderRowCount = intPtr(0)
}
table, _ := xml.Marshal(t)
table, err := xml.Marshal(t)
f.saveFileList(tableXML, table)
return nil
return err
}
// AutoFilter provides the method to add auto filter in a worksheet by given
@ -341,8 +477,7 @@ func (f *File) AutoFilter(sheet, rangeRef string, opts []AutoFilterOptions) erro
}
_ = sortCoordinates(coordinates)
// Correct reference range, such correct C1:B3 to B1:C3.
ref, _ := f.coordinatesToRangeRef(coordinates, true)
filterDB := "_xlnm._FilterDatabase"
ref, _ := coordinatesToRangeRef(coordinates, true)
wb, err := f.workbookReader()
if err != nil {
return err
@ -353,7 +488,7 @@ func (f *File) AutoFilter(sheet, rangeRef string, opts []AutoFilterOptions) erro
}
filterRange := fmt.Sprintf("'%s'!%s", sheet, ref)
d := xlsxDefinedName{
Name: filterDB,
Name: builtInDefinedNames[3],
Hidden: true,
LocalSheetID: intPtr(sheetID),
Data: filterRange,
@ -365,8 +500,11 @@ func (f *File) AutoFilter(sheet, rangeRef string, opts []AutoFilterOptions) erro
} else {
var definedNameExists bool
for idx := range wb.DefinedNames.DefinedName {
definedName := wb.DefinedNames.DefinedName[idx]
if definedName.Name == filterDB && *definedName.LocalSheetID == sheetID && definedName.Hidden {
definedName, localSheetID := wb.DefinedNames.DefinedName[idx], 0
if definedName.LocalSheetID != nil {
localSheetID = *definedName.LocalSheetID
}
if definedName.Name == builtInDefinedNames[3] && localSheetID == sheetID && definedName.Hidden {
wb.DefinedNames.DefinedName[idx].Data = filterRange
definedNameExists = true
}
@ -404,12 +542,12 @@ func (f *File) autoFilter(sheet, ref string, columns, col int, opts []AutoFilter
}
offset := fsCol - col
if offset < 0 || offset > columns {
return fmt.Errorf("incorrect index of column '%s'", opt.Column)
return newInvalidAutoFilterColumnError(opt.Column)
}
fc := &xlsxFilterColumn{ColID: offset}
token := expressionFormat.FindAllString(opt.Expression, -1)
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)
if err != nil {
@ -538,7 +676,7 @@ func (f *File) parseFilterTokens(expression string, tokens []string) ([]int, str
if re {
// Only allow Equals or NotEqual in this context.
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)
// The operator should always be 2 (=) to flag a "simple" equality in

View File

@ -27,6 +27,10 @@ func TestAddTable(t *testing.T) {
ShowHeaderRow: boolPtr(false),
}))
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)
@ -41,11 +45,11 @@ func TestAddTable(t *testing.T) {
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAddTable.xlsx")))
// 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
f = NewFile()
assert.EqualError(t, f.addTable("sheet1", "", 0, 0, 0, 0, 0, nil), "invalid cell reference [0, 0]")
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", "", 0, 0, 0, 0, 0, nil))
assert.Equal(t, newCoordinatesToCellNameError(0, 0), f.addTable("sheet1", "", 1, 1, 0, 0, 0, nil))
// Test set defined name and add table with invalid name
for _, cases := range []struct {
name string
@ -60,24 +64,82 @@ func TestAddTable(t *testing.T) {
{name: "\u0f5f\u0fb3\u0f0b\u0f21", err: newInvalidNameError("\u0f5f\u0fb3\u0f0b\u0f21")},
{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",
Name: cases.name,
}), cases.err.Error())
assert.EqualError(t, f.SetDefinedName(&DefinedName{
}))
assert.Equal(t, cases.err, f.SetDefinedName(&DefinedName{
Name: cases.name, RefersTo: "Sheet1!$A$2:$D$5",
}), cases.err.Error())
}))
}
// 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()
_, err := f.setTableHeader("Sheet1", true, 1, 0, 1)
assert.EqualError(t, err, "invalid cell reference [1, 0]")
// Test get tables in none table worksheet
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) {
@ -102,14 +164,18 @@ func TestAutoFilter(t *testing.T) {
}
// 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
assert.EqualError(t, f.AutoFilter("Sheet1", "A:B1", nil), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
assert.EqualError(t, f.AutoFilter("Sheet1", "A1:B", nil), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")).Error())
assert.Equal(t, newCellNameToCoordinatesError("A", newInvalidCellNameError("A")), f.AutoFilter("Sheet1", "A:B1", nil))
assert.Equal(t, newCellNameToCoordinatesError("B", newInvalidCellNameError("B")), f.AutoFilter("Sheet1", "A1:B", nil))
// Test add auto filter with unsupported charset workbook
f.WorkBook = nil
f.Pkg.Store(defaultXMLPathWorkbook, MacintoshCyrillicCharset)
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) {
@ -131,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",
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: "-",
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",
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",
Expression: "-",
}}), `incorrect number of tokens in criteria '-'`)
}}))
}
func TestParseFilterTokens(t *testing.T) {
@ -156,5 +222,5 @@ func TestParseFilterTokens(t *testing.T) {
assert.EqualError(t, err, "unknown operator: !")
// Test invalid operator in context
_, _, 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
// the LICENSE file.
//
@ -7,26 +7,510 @@
// 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.
// data. This library needs Go version 1.18 or later.
//
// This file contains default templates for XML files we don't yet populated
// based on content.
package excelize
import "encoding/xml"
// 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"}
NameSpaceDrawingMLA14 = xml.Attr{Name: xml.Name{Local: "a14", Space: "xmlns"}, Value: "http://schemas.microsoft.com/office/drawing/2010/main"}
NameSpaceDrawingMLChart = xml.Attr{Name: xml.Name{Local: "c", Space: "xmlns"}, Value: "http://schemas.openxmlformats.org/drawingml/2006/chart"}
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"
defaultXMLPathCalcChain = "xl/calcChain.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"
defaultTempFileSST = "sharedStrings"
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 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>`

875
vml.go

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
// the LICENSE file.
//
@ -7,7 +7,7 @@
// 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.
// data. This library needs Go version 1.18 or later.
package excelize
@ -20,7 +20,7 @@ type vmlDrawing struct {
XMLNSv string `xml:"xmlns:v,attr"`
XMLNSo string `xml:"xmlns:o,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"`
ShapeType *xlsxShapeType `xml:"v:shapetype"`
Shape []xlsxShape `xml:"v:shape"`
@ -44,11 +44,12 @@ type xlsxIDmap struct {
type xlsxShape struct {
XMLName xml.Name `xml:"v:shape"`
ID string `xml:"id,attr"`
SpID string `xml:"o:spid,attr,omitempty"`
Type string `xml:"type,attr"`
Style string `xml:"style,attr"`
Button string `xml:"o:button,attr,omitempty"`
Filled string `xml:"filled,attr,omitempty"`
FillColor string `xml:"fillcolor,attr"`
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"`
@ -60,9 +61,14 @@ type xlsxShapeType struct {
ID string `xml:"id,attr"`
CoordSize string `xml:"coordsize,attr"`
Spt int `xml:"o:spt,attr"`
PreferRelative string `xml:"o:preferrelative,attr,omitempty"`
Path string `xml:"path,attr"`
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.
@ -72,10 +78,28 @@ type xlsxStroke struct {
// vPath directly maps the v:path element.
type vPath struct {
ExtrusionOK string `xml:"o:extrusionok,attr,omitempty"`
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
// Shape element.
type vFill struct {
@ -106,6 +130,13 @@ type vTextBox struct {
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.
type xlsxDiv struct {
Style string `xml:"style,attr"`
@ -128,18 +159,28 @@ type vmlFont struct {
// element.
type xClientData struct {
ObjectType string `xml:"ObjectType,attr"`
MoveWithCells string `xml:"x:MoveWithCells"`
SizeWithCells string `xml:"x:SizeWithCells"`
MoveWithCells *string `xml:"x:MoveWithCells"`
SizeWithCells *string `xml:"x:SizeWithCells"`
Anchor string `xml:"x:Anchor"`
AutoFill string `xml:"x:AutoFill"`
Row int `xml:"x:Row"`
Column int `xml:"x:Column"`
Locked string `xml:"x:Locked,omitempty"`
PrintObject string `xml:"x:PrintObject,omitempty"`
AutoFill string `xml:"x:AutoFill,omitempty"`
FmlaMacro string `xml:"x:FmlaMacro,omitempty"`
TextHAlign string `xml:"x:TextHAlign,omitempty"`
TextVAlign string `xml:"x:TextVAlign,omitempty"`
Checked *string `xml:"x:Checked,omitempty"`
NoThreeD *string `xml:"x:NoThreeD,omitempty"`
FirstButton *string `xml:"x:FirstButton,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
@ -155,17 +196,21 @@ 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.
type decodeShape struct {
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"`
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"`
@ -175,15 +220,67 @@ type decodeShape struct {
// 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"`
Column int
Row int
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.
@ -192,26 +289,34 @@ type encodeShape struct {
Shadow *vShadow `xml:"v:shadow"`
Path *vPath `xml:"v:path"`
TextBox *vTextBox `xml:"v:textbox"`
ImageData *vImageData `xml:"v:imagedata"`
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 {
rows int
cols int
FormCtrl bool
Sheet string
Author string
AuthorID int
Cell string
Checked bool
Text string
Macro string
Width uint
Height uint
Paragraph []RichTextRun
Type FormControlType
Format GraphicOptions
formCtrl bool
sheet string
Comment
FormControl
}
// FormControl directly maps the form controls information.
@ -221,8 +326,27 @@ type FormControl struct {
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
}

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
// the LICENSE file.
//
@ -7,12 +7,13 @@
// 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.
// data. This library needs Go version 1.18 or later.
package excelize
import (
"encoding/xml"
"fmt"
"os"
"path/filepath"
"strings"
@ -34,7 +35,7 @@ func TestAddComment(t *testing.T) {
// 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.EqualError(t, f.AddComment("Sheet1", Comment{Cell: "A", Author: "Excelize", Paragraph: []RichTextRun{{Text: "Excelize: ", Font: &Font{Bold: true}}, {Text: "This is a comment."}}}), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
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)
@ -56,7 +57,7 @@ func TestAddComment(t *testing.T) {
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())
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
@ -104,7 +105,7 @@ func TestDeleteComment(t *testing.T) {
assert.Len(t, comments, 0)
// Test delete comment with invalid sheet name
assert.EqualError(t, f.DeleteComment("Sheet:1", "A1"), ErrSheetNameInvalid.Error())
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"))
@ -121,6 +122,10 @@ func TestDeleteComment(t *testing.T) {
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) {
@ -149,26 +154,21 @@ func TestCountComments(t *testing.T) {
func TestAddDrawingVML(t *testing.T) {
// Test addDrawingVML with illegal cell reference
f := NewFile()
assert.EqualError(t, f.addDrawingVML(0, "", &vmlOptions{Cell: "*"}), newCellNameToCoordinatesError("*", newInvalidCellNameError("*")).Error())
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{Cell: "A1"}), "XML syntax error on line 1: invalid UTF-8")
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()
assert.NoError(t, f.AddFormControl("Sheet1", FormControl{
Cell: "D1",
Type: FormControlButton,
Macro: "Button1_Click",
}))
assert.NoError(t, f.AddFormControl("Sheet1", FormControl{
Cell: "A1",
Type: FormControlButton,
Macro: "Button1_Click",
Width: 140,
Height: 60,
Text: "Button 1\r\n",
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{
@ -182,29 +182,65 @@ func TestFormControl(t *testing.T) {
Text: "C1=A1+B1",
},
},
}))
assert.NoError(t, f.AddFormControl("Sheet1", FormControl{
Cell: "A5",
Type: FormControlCheckbox,
Text: "Check Box 1",
Checked: true,
}))
assert.NoError(t, f.AddFormControl("Sheet1", FormControl{
Cell: "A6",
Type: FormControlCheckbox,
Text: "Check Box 2",
}))
assert.NoError(t, f.AddFormControl("Sheet1", FormControl{
Cell: "A7",
Type: FormControlRadio,
Text: "Option Button 1",
Checked: true,
}))
assert.NoError(t, f.AddFormControl("Sheet1", FormControl{
Cell: "A8",
Type: FormControlRadio,
Text: "Option Button 2",
}))
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)
@ -213,33 +249,53 @@ func TestFormControl(t *testing.T) {
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",
Text: "Button 2",
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",
Cell: "A1", Type: 0x37, Macro: "Button1_Click",
}), ErrParameterInvalid)
// Test add form control on not exists worksheet
assert.Equal(t, f.AddFormControl("SheetN", FormControl{
Cell: "A1",
Type: FormControlButton,
Macro: "Button1_Click",
}), newNoExistSheetError("SheetN"))
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, f.DeleteFormControl("SheetN", "A1"), newNoExistSheetError("SheetN"))
// 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())
@ -248,8 +304,208 @@ func TestFormControl(t *testing.T) {
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
// the LICENSE file.
//
@ -7,7 +7,7 @@
// 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.
// data. This library needs Go version 1.18 or later.
package excelize
@ -170,6 +170,26 @@ func (f *File) getWorkbookRelsPath() (path string) {
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
// structure after deserialization.
func (f *File) workbookReader() (*xlsxWorkbook, error) {
@ -177,9 +197,13 @@ func (f *File) workbookReader() (*xlsxWorkbook, error) {
if f.WorkBook == nil {
wbPath := f.getWorkbookPath()
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))))
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)
}
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)))
}
}
// 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()
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
// the LICENSE file.
//
@ -7,7 +7,7 @@
// 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.
// data. This library needs Go version 1.18 or later.
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
// the LICENSE file.
//
@ -7,13 +7,14 @@
// 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.
// data. This library needs Go version 1.18 or later.
package excelize
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 {
XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main calcChain"`
C []xlsxCalcChainC `xml:"c"`
@ -76,9 +77,48 @@ type xlsxCalcChain struct {
// | boolean datatype.
type xlsxCalcChainC struct {
R string `xml:"r,attr"`
I int `xml:"i,attr"`
I int `xml:"i,attr,omitempty"`
L bool `xml:"l,attr,omitempty"`
S bool `xml:"s,attr,omitempty"`
T bool `xml:"t,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
// the LICENSE file.
//
@ -7,7 +7,7 @@
// 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.
// data. This library needs Go version 1.18 or later.
package excelize
@ -252,7 +252,7 @@ type aLn struct {
Cap string `xml:"cap,attr,omitempty"`
Cmpd string `xml:"cmpd,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"`
SolidFill *aSolidFill `xml:"a:solidFill"`
}
@ -301,21 +301,21 @@ type cView3D struct {
// plot area of the chart.
type cPlotArea struct {
Layout *string `xml:"layout"`
AreaChart *cCharts `xml:"areaChart"`
Area3DChart *cCharts `xml:"area3DChart"`
BarChart *cCharts `xml:"barChart"`
Bar3DChart *cCharts `xml:"bar3DChart"`
BubbleChart *cCharts `xml:"bubbleChart"`
DoughnutChart *cCharts `xml:"doughnutChart"`
LineChart *cCharts `xml:"lineChart"`
Line3DChart *cCharts `xml:"line3DChart"`
PieChart *cCharts `xml:"pieChart"`
Pie3DChart *cCharts `xml:"pie3DChart"`
OfPieChart *cCharts `xml:"ofPieChart"`
RadarChart *cCharts `xml:"radarChart"`
ScatterChart *cCharts `xml:"scatterChart"`
Surface3DChart *cCharts `xml:"surface3DChart"`
SurfaceChart *cCharts `xml:"surfaceChart"`
AreaChart []*cCharts `xml:"areaChart"`
Area3DChart []*cCharts `xml:"area3DChart"`
BarChart []*cCharts `xml:"barChart"`
Bar3DChart []*cCharts `xml:"bar3DChart"`
BubbleChart []*cCharts `xml:"bubbleChart"`
DoughnutChart []*cCharts `xml:"doughnutChart"`
LineChart []*cCharts `xml:"lineChart"`
Line3DChart []*cCharts `xml:"line3DChart"`
PieChart []*cCharts `xml:"pieChart"`
Pie3DChart []*cCharts `xml:"pie3DChart"`
OfPieChart []*cCharts `xml:"ofPieChart"`
RadarChart []*cCharts `xml:"radarChart"`
ScatterChart []*cCharts `xml:"scatterChart"`
Surface3DChart []*cCharts `xml:"surface3DChart"`
SurfaceChart []*cCharts `xml:"surfaceChart"`
CatAx []*cAxs `xml:"catAx"`
ValAx []*cAxs `xml:"valAx"`
SerAx []*cAxs `xml:"serAx"`
@ -483,6 +483,7 @@ type cNumCache struct {
// the specific formatting and positioning settings.
type cDLbls struct {
NumFmt *cNumFmt `xml:"numFmt"`
DLblPos *attrValString `xml:"dLblPos"`
ShowLegendKey *attrValBool `xml:"showLegendKey"`
ShowVal *attrValBool `xml:"showVal"`
ShowCatName *attrValBool `xml:"showCatName"`
@ -533,11 +534,13 @@ type ChartAxis struct {
MajorGridLines bool
MinorGridLines bool
MajorUnit float64
TickLabelPosition ChartTickLabelPositionType
TickLabelSkip int
ReverseOrder bool
Secondary bool
Maximum *float64
Minimum *float64
Alignment Alignment
Font Font
LogBase float64
NumFmt ChartNumFmt
@ -560,6 +563,7 @@ type ChartPlotArea struct {
ShowPercent bool
ShowSerName bool
ShowVal bool
Fill Fill
NumFmt ChartNumFmt
}
@ -570,12 +574,15 @@ type Chart struct {
Format GraphicOptions
Dimension ChartDimension
Legend ChartLegend
Title ChartTitle
Title []RichTextRun
VaryColors *bool
XAxis ChartAxis
YAxis ChartAxis
PlotArea ChartPlotArea
Fill Fill
Border ChartLine
ShowBlanksAs string
BubbleSize int
HoleSize int
order int
}
@ -588,12 +595,14 @@ type ChartLegend struct {
// ChartMarker directly maps the format settings of the chart marker.
type ChartMarker struct {
Fill Fill
Symbol string
Size int
}
// ChartLine directly maps the format settings of the chart line.
type ChartLine struct {
Type ChartLineType
Smooth bool
Width float64
}
@ -602,14 +611,10 @@ type ChartLine struct {
type ChartSeries struct {
Name string
Categories string
Sizes string
Values string
Sizes string
Fill Fill
Line ChartLine
Marker ChartMarker
}
// ChartTitle directly maps the format settings of the chart title.
type ChartTitle struct {
Name string
DataLabelPosition ChartDataLabelPositionType
}

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
// the LICENSE file.
//
@ -9,7 +9,7 @@
// 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.
// data. This library needs Go version 1.18 or later.
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
// the LICENSE file.
//
@ -7,7 +7,7 @@
// 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.
// data. This library needs Go version 1.18 or later.
package excelize
@ -78,5 +78,7 @@ type Comment struct {
AuthorID int
Cell string
Text string
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
// the LICENSE file.
//
@ -7,7 +7,7 @@
// 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.
// data. This library needs Go version 1.18 or later.
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
// the LICENSE file.
//
@ -7,7 +7,7 @@
// 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.
// data. This library needs Go version 1.18 or later.
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
// the LICENSE file.
//
@ -7,7 +7,7 @@
// 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.
// data. This library needs Go version 1.18 or later.
package excelize
@ -22,23 +22,63 @@ type decodeCellAnchor struct {
From *decodeFrom `xml:"from"`
To *decodeTo `xml:"to"`
Sp *decodeSp `xml:"sp"`
Pic *decodePic `xml:"pic"`
ClientData *decodeClientData `xml:"clientData"`
AlternateContent []*xlsxAlternateContent `xml:"AlternateContent"`
Content string `xml:",innerxml"`
}
// 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.
// decodeCellAnchorPos defines the structure used to deserialize the cell anchor
// for adjust drawing object on inserting/deleting column/rows.
type decodeCellAnchorPos struct {
EditAs string `xml:"editAs,attr,omitempty"`
From *xlsxFrom `xml:"from"`
To *xlsxTo `xml:"to"`
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 {
Macro string `xml:"macro,attr,omitempty"`
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 is a container for the non-visual identification properties, shape
// 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.
type decodeNvSpPr struct {
CNvPr *decodeCNvPr `xml:"cNvPr"`
ExtLst *decodeExt `xml:"extLst"`
ExtLst *decodeAExt `xml:"extLst"`
CNvSpPr *decodeCNvSpPr `xml:"cNvSpPr"`
}
@ -63,7 +103,7 @@ type decodeCNvSpPr struct {
// changed after serialization and deserialization, two different structures
// are defined. decodeWsDr just for deserialization.
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"`
Xdr string `xml:"xmlns xdr,attr"`
R string `xml:"xmlns r,attr"`
@ -72,22 +112,12 @@ type decodeWsDr struct {
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
// element specifies non-visual canvas properties. This allows for additional
// information that does not affect the appearance of the picture to be
// stored.
type decodeCNvPr struct {
XMLName xml.Name `xml:"cNvPr"`
ID int `xml:"id,attr"`
Name string `xml:"name,attr"`
Descr string `xml:"descr,attr"`
@ -134,8 +164,8 @@ type decodeOff struct {
Y int `xml:"y,attr"`
}
// decodeExt directly maps the ext element.
type decodeExt struct {
// decodeAExt directly maps the a:ext element.
type decodeAExt struct {
Cx int `xml:"cx,attr"`
Cy int `xml:"cy,attr"`
}
@ -154,7 +184,7 @@ type decodePrstGeom struct {
// be for a shape or group shape.
type decodeXfrm struct {
Off decodeOff `xml:"off"`
Ext decodeExt `xml:"ext"`
Ext decodeAExt `xml:"ext"`
}
// decodeCNvPicPr directly maps the cNvPicPr (Non-Visual Picture Drawing
@ -232,3 +262,15 @@ type decodeClientData struct {
FLocksWithSheet bool `xml:"fLocksWithSheet,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
// the LICENSE file.
//
@ -7,7 +7,7 @@
// 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.
// data. This library needs Go version 1.18 or later.
package excelize
@ -16,208 +16,6 @@ import (
"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 = 260
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
// element specifies non-visual canvas properties. This allows for additional
// 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"`
}
// xlsxExt directly maps the ext element.
type xlsxExt struct {
// aExt directly maps the a:ext element.
type aExt struct {
Cx int `xml:"cx,attr"`
Cy int `xml:"cy,attr"`
}
@ -305,7 +103,7 @@ type xlsxPrstGeom struct {
// be for a shape or group shape.
type xlsxXfrm struct {
Off xlsxOff `xml:"a:off"`
Ext xlsxExt `xml:"a:ext"`
Ext aExt `xml:"a:ext"`
}
// xlsxCNvPicPr directly maps the cNvPicPr (Non-Visual Picture Drawing
@ -364,6 +162,7 @@ type xlsxBlipFill struct {
// maximum value of less than or equal to 20116800.
type xlsxLineProperties struct {
W int `xml:"w,attr,omitempty"`
SolidFill *xlsxInnerXML `xml:"a:solidFill"`
}
// xlsxSpPr directly maps the spPr (Shape Properties). This element specifies
@ -374,6 +173,7 @@ type xlsxLineProperties struct {
type xlsxSpPr struct {
Xfrm xlsxXfrm `xml:"a:xfrm"`
PrstGeom xlsxPrstGeom `xml:"a:prstGeom"`
SolidFill *xlsxInnerXML `xml:"a:solidFill"`
Ln xlsxLineProperties `xml:"a:ln"`
}
@ -414,22 +214,55 @@ type xdrClientData struct {
FPrintsWithSheet bool `xml:"fPrintsWithSheet,attr"`
}
// xdrCellAnchor 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.
// xdrCellAnchor specifies a oneCellAnchor (One Cell Anchor Shape Size) and
// twoCellAnchor (Two Cell Anchor Shape Size) placeholder for a group, a shape,
// or a drawing element. It moves with cells and its extents are in EMU units.
type xdrCellAnchor struct {
EditAs string `xml:"editAs,attr,omitempty"`
Pos *xlsxPoint2D `xml:"xdr:pos"`
From *xlsxFrom `xml:"xdr:from"`
To *xlsxTo `xml:"xdr:to"`
Ext *xlsxExt `xml:"xdr:ext"`
Ext *aExt `xml:"xdr:ext"`
Sp *xdrSp `xml:"xdr:sp"`
Pic *xlsxPic `xml:"xdr:pic,omitempty"`
GraphicFrame string `xml:",innerxml"`
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.
type xlsxPoint2D struct {
XMLName xml.Name `xml:"xdr:pos"`
@ -442,6 +275,7 @@ type xlsxPoint2D struct {
type xlsxWsDr struct {
mu sync.Mutex
XMLName xml.Name `xml:"xdr:wsDr"`
NS string `xml:"xmlns,attr,omitempty"`
A string `xml:"xmlns:a,attr,omitempty"`
Xdr string `xml:"xmlns:xdr,attr,omitempty"`
R string `xml:"xmlns:r,attr,omitempty"`
@ -491,6 +325,12 @@ type xlsxGraphic struct {
type xlsxGraphicData struct {
URI string `xml:"uri,attr"`
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.
@ -508,6 +348,7 @@ type xlsxChart struct {
// This shape is specified along with all other shapes within either the shape
// tree or group shape elements.
type xdrSp struct {
XMLName xml.Name `xml:"xdr:sp"`
Macro string `xml:"macro,attr"`
Textlink string `xml:"textlink,attr"`
NvSpPr *xdrNvSpPr `xml:"xdr:nvSpPr"`
@ -586,6 +427,7 @@ type Picture struct {
Extension string
File []byte
Format *GraphicOptions
InsertType PictureInsertType
}
// GraphicOptions directly maps the format settings of the picture.
@ -595,6 +437,7 @@ type GraphicOptions struct {
Locked *bool
LockAspectRatio bool
AutoFit bool
AutoFitIgnoreAspect bool
OffsetX int
OffsetY int
ScaleX float64
@ -617,13 +460,6 @@ type Shape struct {
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.
type ShapeLine struct {
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
// the LICENSE file.
//
@ -7,7 +7,7 @@
// 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.
// data. This library needs Go version 1.18 or later.
package excelize
@ -134,12 +134,12 @@ type xlsxSharedItems struct {
MaxDate string `xml:"maxDate,attr,omitempty"`
Count int `xml:"count,attr"`
LongText bool `xml:"longText,attr,omitempty"`
M *xlsxMissing `xml:"m"`
N *xlsxNumber `xml:"n"`
B *xlsxBoolean `xml:"b"`
E *xlsxError `xml:"e"`
S *xlsxString `xml:"s"`
D *xlsxDateTime `xml:"d"`
M []xlsxMissing `xml:"m"`
N []xlsxNumber `xml:"n"`
B []xlsxBoolean `xml:"b"`
E []xlsxError `xml:"e"`
S []xlsxString `xml:"s"`
D []xlsxDateTime `xml:"d"`
}
// xlsxMissing represents a value that was not specified.
@ -226,3 +226,17 @@ type xlsxMeasureGroups struct{}
// xlsxMaps represents the PivotTable OLAP measure group - Dimension maps.
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
// the LICENSE file.
//
@ -7,7 +7,7 @@
// 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.
// data. This library needs Go version 1.18 or later.
package excelize
@ -56,15 +56,15 @@ type xlsxPivotTableDefinition struct {
EnableDrill bool `xml:"enableDrill,attr,omitempty"`
EnableFieldProperties bool `xml:"enableFieldProperties,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"`
PageOverThenDown *bool `xml:"pageOverThenDown,attr,omitempty"`
PageOverThenDown *bool `xml:"pageOverThenDown,attr"`
SubtotalHiddenItems bool `xml:"subtotalHiddenItems,attr,omitempty"`
RowGrandTotals *bool `xml:"rowGrandTotals,attr,omitempty"`
ColGrandTotals *bool `xml:"colGrandTotals,attr,omitempty"`
RowGrandTotals *bool `xml:"rowGrandTotals,attr"`
ColGrandTotals *bool `xml:"colGrandTotals,attr"`
FieldPrintTitles bool `xml:"fieldPrintTitles,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"`
CreatedVersion int `xml:"createdVersion,attr,omitempty"`
Indent int `xml:"indent,attr,omitempty"`
@ -74,7 +74,7 @@ type xlsxPivotTableDefinition struct {
Compact *bool `xml:"compact,attr"`
Outline *bool `xml:"outline,attr"`
OutlineData bool `xml:"outlineData,attr,omitempty"`
CompactData *bool `xml:"compactData,attr,omitempty"`
CompactData *bool `xml:"compactData,attr"`
Published bool `xml:"published,attr,omitempty"`
GridDropZones bool `xml:"gridDropZones,attr,omitempty"`
Immersive bool `xml:"immersive,attr,omitempty"`
@ -150,7 +150,7 @@ type xlsxPivotField struct {
DataSourceSort bool `xml:"dataSourceSort,attr,omitempty"`
NonAutoSortDefault bool `xml:"nonAutoSortDefault,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"`
CountASubtotal bool `xml:"countASubtotal,attr,omitempty"`
AvgSubtotal bool `xml:"avgSubtotal,attr,omitempty"`
@ -273,7 +273,7 @@ type xlsxDataField struct {
ShowDataAs string `xml:"showDataAs,attr,omitempty"`
BaseField int `xml:"baseField,attr,omitempty"`
BaseItem int64 `xml:"baseItem,attr,omitempty"`
NumFmtID string `xml:"numFmtId,attr,omitempty"`
NumFmtID int `xml:"numFmtId,attr,omitempty"`
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
// the LICENSE file.
//
@ -7,7 +7,7 @@
// 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.
// data. This library needs Go version 1.18 or later.
package excelize

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
// the LICENSE file.
//
@ -7,7 +7,7 @@
// 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.
// data. This library needs Go version 1.18 or later.
package excelize
@ -251,18 +251,13 @@ type xlsxDxfs struct {
// xlsxDxf directly maps the dxf element. A single dxf record, expressing
// incremental formatting to be applied.
type xlsxDxf struct {
Dxf string `xml:",innerxml"`
}
// dxf directly maps the dxf element.
type dxf struct {
Font *xlsxFont `xml:"font"`
NumFmt *xlsxNumFmt `xml:"numFmt"`
Fill *xlsxFill `xml:"fill"`
Alignment *xlsxAlignment `xml:"alignment"`
Border *xlsxBorder `xml:"border"`
Protection *xlsxProtection `xml:"protection"`
ExtLst *xlsxExt `xml:"extLst"`
ExtLst *aExt `xml:"extLst"`
}
// xlsxTableStyles directly maps the tableStyles element. This element
@ -301,7 +296,14 @@ type xlsxNumFmts struct {
// of a cell.
type xlsxNumFmt struct {
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"`
}
// xlsxIndexedColors directly maps the single ARGB entry for the corresponding
// color index.
type xlsxIndexedColors struct {
RgbColor []xlsxColor `xml:"rgbColor"`
}
// xlsxStyleColors directly maps the colors' element. Color information
@ -309,7 +311,8 @@ type xlsxNumFmt struct {
// legacy color palette has been modified (backwards compatibility settings) or
// a custom color has been selected while using this workbook.
type xlsxStyleColors struct {
Color string `xml:",innerxml"`
IndexedColors *xlsxIndexedColors `xml:"indexedColors"`
MruColors *xlsxInnerXML `xml:"mruColors"`
}
// Alignment directly maps the alignment settings of the cells.

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
// the LICENSE file.
//
@ -7,7 +7,7 @@
// 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.
// data. This library needs Go version 1.18 or later.
package excelize
@ -21,22 +21,28 @@ import "encoding/xml"
type xlsxTable struct {
XMLName xml.Name `xml:"table"`
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"`
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"`
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"`
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"`
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"`
TableColumns *xlsxTableColumns `xml:"tableColumns"`
TableStyleInfo *xlsxTableStyleInfo `xml:"tableStyleInfo"`
@ -171,18 +177,18 @@ type xlsxTableColumns struct {
// xlsxTableColumn directly maps the element representing a single column for
// this table.
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"`
UniqueName string `xml:"uniqueName,attr,omitempty"`
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"`
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
@ -196,8 +202,40 @@ type xlsxTableStyleInfo struct {
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.
type Table struct {
tID int
rID string
tableXML string
Range string
Name 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
// the LICENSE file.
//
@ -7,7 +7,7 @@
// 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.
// data. This library needs Go version 1.18 or later.
package excelize
@ -16,15 +16,15 @@ import "encoding/xml"
// xlsxTheme directly maps the theme element in the namespace
// http://schemas.openxmlformats.org/drawingml/2006/main
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"`
XMLNSr string `xml:"xmlns:r,attr"`
Name string `xml:"name,attr"`
ThemeElements xlsxBaseStyles `xml:"themeElements"`
ObjectDefaults xlsxObjectDefaults `xml:"objectDefaults"`
ExtraClrSchemeLst xlsxExtraClrSchemeLst `xml:"extraClrSchemeLst"`
CustClrLst *xlsxInnerXML `xml:"custClrLst"`
ExtLst *xlsxExtLst `xml:"extLst"`
ThemeElements xlsxBaseStyles `xml:"a:themeElements"`
ObjectDefaults xlsxObjectDefaults `xml:"a:objectDefaults"`
ExtraClrSchemeLst xlsxExtraClrSchemeLst `xml:"a:extraClrSchemeLst"`
CustClrLst *xlsxInnerXML `xml:"a:custClrLst"`
ExtLst *xlsxExtLst `xml:"a:extLst"`
}
// 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
// different formatting options for different pieces of a document.
type xlsxBaseStyles struct {
ClrScheme xlsxColorScheme `xml:"clrScheme"`
FontScheme xlsxFontScheme `xml:"fontScheme"`
FmtScheme xlsxStyleMatrix `xml:"fmtScheme"`
ExtLst *xlsxExtLst `xml:"extLst"`
ClrScheme xlsxColorScheme `xml:"a:clrScheme"`
FontScheme xlsxFontScheme `xml:"a:fontScheme"`
FmtScheme xlsxStyleMatrix `xml:"a:fmtScheme"`
ExtLst *xlsxExtLst `xml:"a:extLst"`
}
// xlsxCTColor holds the actual color values that are to be applied to a given
// diagram and how those colors are to be applied.
type xlsxCTColor 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"`
ScrgbClr *xlsxInnerXML `xml:"a:scrgbClr"`
SrgbClr *attrValString `xml:"a:srgbClr"`
HslClr *xlsxInnerXML `xml:"a:hslClr"`
SysClr *xlsxSysClr `xml:"a:sysClr"`
SchemeClr *xlsxInnerXML `xml:"a:schemeClr"`
PrstClr *xlsxInnerXML `xml:"a:prstClr"`
}
// 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.
type xlsxColorScheme struct {
Name string `xml:"name,attr"`
Dk1 xlsxCTColor `xml:"dk1"`
Lt1 xlsxCTColor `xml:"lt1"`
Dk2 xlsxCTColor `xml:"dk2"`
Lt2 xlsxCTColor `xml:"lt2"`
Accent1 xlsxCTColor `xml:"accent1"`
Accent2 xlsxCTColor `xml:"accent2"`
Accent3 xlsxCTColor `xml:"accent3"`
Accent4 xlsxCTColor `xml:"accent4"`
Accent5 xlsxCTColor `xml:"accent5"`
Accent6 xlsxCTColor `xml:"accent6"`
Hlink xlsxCTColor `xml:"hlink"`
FolHlink xlsxCTColor `xml:"folHlink"`
ExtLst *xlsxExtLst `xml:"extLst"`
Dk1 xlsxCTColor `xml:"a:dk1"`
Lt1 xlsxCTColor `xml:"a:lt1"`
Dk2 xlsxCTColor `xml:"a:dk2"`
Lt2 xlsxCTColor `xml:"a:lt2"`
Accent1 xlsxCTColor `xml:"a:accent1"`
Accent2 xlsxCTColor `xml:"a:accent2"`
Accent3 xlsxCTColor `xml:"a:accent3"`
Accent4 xlsxCTColor `xml:"a:accent4"`
Accent5 xlsxCTColor `xml:"a:accent5"`
Accent6 xlsxCTColor `xml:"a:accent6"`
Hlink xlsxCTColor `xml:"a:hlink"`
FolHlink xlsxCTColor `xml:"a:folHlink"`
ExtLst *xlsxExtLst `xml:"a:extLst"`
}
// 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
// define a font for use in a specific language or languages.
type xlsxFontCollection struct {
Latin *xlsxCTTextFont `xml:"latin"`
Ea *xlsxCTTextFont `xml:"ea"`
Cs *xlsxCTTextFont `xml:"cs"`
Font []xlsxCTSupplementalFont `xml:"font"`
ExtLst *xlsxExtLst `xml:"extLst"`
Latin *xlsxCTTextFont `xml:"a:latin"`
Ea *xlsxCTTextFont `xml:"a:ea"`
Cs *xlsxCTTextFont `xml:"a:cs"`
Font []xlsxCTSupplementalFont `xml:"a:font"`
ExtLst *xlsxExtLst `xml:"a:extLst"`
}
// xlsxFontScheme element defines the font scheme within the theme. The font
@ -109,9 +109,9 @@ type xlsxFontCollection struct {
// paragraph areas.
type xlsxFontScheme struct {
Name string `xml:"name,attr"`
MajorFont xlsxFontCollection `xml:"majorFont"`
MinorFont xlsxFontCollection `xml:"minorFont"`
ExtLst *xlsxExtLst `xml:"extLst"`
MajorFont xlsxFontCollection `xml:"a:majorFont"`
MinorFont xlsxFontCollection `xml:"a:minorFont"`
ExtLst *xlsxExtLst `xml:"a:extLst"`
}
// xlsxStyleMatrix defines a set of formatting options, which can be referenced
@ -121,10 +121,10 @@ type xlsxFontScheme struct {
// change when the theme is changed.
type xlsxStyleMatrix struct {
Name string `xml:"name,attr,omitempty"`
FillStyleLst xlsxFillStyleLst `xml:"fillStyleLst"`
LnStyleLst xlsxLnStyleLst `xml:"lnStyleLst"`
EffectStyleLst xlsxEffectStyleLst `xml:"effectStyleLst"`
BgFillStyleLst xlsxBgFillStyleLst `xml:"bgFillStyleLst"`
FillStyleLst xlsxFillStyleLst `xml:"a:fillStyleLst"`
LnStyleLst xlsxLnStyleLst `xml:"a:lnStyleLst"`
EffectStyleLst xlsxEffectStyleLst `xml:"a:effectStyleLst"`
BgFillStyleLst xlsxBgFillStyleLst `xml:"a:bgFillStyleLst"`
}
// xlsxFillStyleLst element defines a set of three fill styles that are used
@ -161,3 +161,85 @@ type xlsxSysClr struct {
Val string `xml:"val,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
// the LICENSE file.
//
@ -7,7 +7,7 @@
// 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.
// data. This library needs Go version 1.18 or later.
package excelize
@ -16,7 +16,8 @@ import (
"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 {
mu sync.Mutex
XMLName xml.Name `xml:"http://schemas.openxmlformats.org/package/2006/relationships Relationships"`
@ -150,7 +151,7 @@ type xlsxWorkBookView struct {
YWindow string `xml:"yWindow,attr,omitempty"`
WindowWidth int `xml:"windowWidth,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"`
ActiveTab int `xml:"activeTab,attr,omitempty"`
AutoFilterDateGrouping *bool `xml:"autoFilterDateGrouping,attr"`
@ -218,6 +219,64 @@ type xlsxExtLst struct {
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
// the collection of defined names for this workbook. Defined names are
// descriptive names to represent cells, ranges of cells, formulas, or constant
@ -309,7 +368,7 @@ type xlsxCustomWorkbookView struct {
ShowSheetTabs *bool `xml:"showSheetTabs,attr"`
ShowStatusbar *bool `xml:"showStatusbar,attr"`
ShowVerticalScroll *bool `xml:"showVerticalScroll,attr"`
TabRatio *int `xml:"tabRatio,attr"`
TabRatio *float64 `xml:"tabRatio,attr"`
WindowHeight *int `xml:"windowHeight,attr"`
WindowWidth *int `xml:"windowWidth,attr"`
XWindow *int `xml:"xWindow,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
// the LICENSE file.
//
@ -7,7 +7,7 @@
// 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.
// data. This library needs Go version 1.18 or later.
package excelize
@ -81,8 +81,8 @@ type xlsxHeaderFooter struct {
XMLName xml.Name `xml:"headerFooter"`
DifferentOddEven bool `xml:"differentOddEven,attr,omitempty"`
DifferentFirst bool `xml:"differentFirst,attr,omitempty"`
ScaleWithDoc bool `xml:"scaleWithDoc,attr,omitempty"`
AlignWithMargins bool `xml:"alignWithMargins,attr,omitempty"`
ScaleWithDoc *bool `xml:"scaleWithDoc,attr"`
AlignWithMargins *bool `xml:"alignWithMargins,attr"`
OddHeader string `xml:"oddHeader,omitempty"`
OddFooter string `xml:"oddFooter,omitempty"`
EvenHeader string `xml:"evenHeader,omitempty"`
@ -424,12 +424,12 @@ type xlsxDataValidations struct {
DisablePrompts bool `xml:"disablePrompts,attr,omitempty"`
XWindow int `xml:"xWindow,attr,omitempty"`
YWindow int `xml:"yWindow,attr,omitempty"`
DataValidation []*DataValidation `xml:"dataValidation"`
DataValidation []*xlsxDataValidation `xml:"dataValidation"`
}
// DataValidation directly maps the single item of data validation defined
// on a range of the worksheet.
type DataValidation struct {
type xlsxDataValidation struct {
AllowBlank bool `xml:"allowBlank,attr"`
Error *string `xml:"error,attr"`
ErrorStyle *string `xml:"errorStyle,attr"`
@ -441,9 +441,10 @@ type DataValidation struct {
ShowErrorMessage bool `xml:"showErrorMessage,attr,omitempty"`
ShowInputMessage bool `xml:"showInputMessage,attr,omitempty"`
Sqref string `xml:"sqref,attr"`
XMSqref string `xml:"sqref,omitempty"`
Type string `xml:"type,attr,omitempty"`
Formula1 string `xml:",innerxml"`
Formula2 string `xml:",innerxml"`
Formula1 *xlsxInnerXML `xml:"formula1"`
Formula2 *xlsxInnerXML `xml:"formula2"`
}
// xlsxC collection represents a cell in the worksheet. Information about the
@ -477,6 +478,7 @@ type xlsxC struct {
F *xlsxF `xml:"f"` // Formula
V string `xml:"v,omitempty"` // Value
IS *xlsxSI `xml:"is"`
f string
}
// xlsxF represents a formula for the cell. The formula expression is
@ -699,33 +701,6 @@ type xlsxLegacyDrawingHF struct {
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.
type decodeX14SparklineGroups struct {
XMLName xml.Name `xml:"sparklineGroups"`
@ -733,8 +708,7 @@ type decodeX14SparklineGroups struct {
Content string `xml:",innerxml"`
}
// decodeX14ConditionalFormattingExt directly maps the ext
// element.
// decodeX14ConditionalFormattingExt directly maps the ext element.
type decodeX14ConditionalFormattingExt struct {
XMLName xml.Name `xml:"ext"`
ID string `xml:"id"`
@ -748,6 +722,14 @@ type decodeX14ConditionalFormattings struct {
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
// element.
type decodeX14ConditionalFormatting struct {
@ -769,7 +751,7 @@ type decodeX14DataBar struct {
MaxLength int `xml:"maxLength,attr"`
MinLength int `xml:"minLength,attr"`
Border bool `xml:"border,attr,omitempty"`
Gradient bool `xml:"gradient,attr"`
Gradient *bool `xml:"gradient,attr"`
ShowValue bool `xml:"showValue,attr,omitempty"`
Direction string `xml:"direction,attr,omitempty"`
Cfvo []*xlsxCfvo `xml:"cfvo"`
@ -863,6 +845,24 @@ type xlsxX14Sparkline struct {
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.
type SparklineOptions struct {
Location []string
@ -917,7 +917,7 @@ type ConditionalFormatOptions struct {
Type string
AboveAverage bool
Percent bool
Format int
Format *int
Criteria string
Value string
MinType string
@ -963,10 +963,10 @@ type SheetProtectionOptions struct {
// HeaderFooterOptions directly maps the settings of header and footer.
type HeaderFooterOptions struct {
AlignWithMargins bool
AlignWithMargins *bool
DifferentFirst bool
DifferentOddEven bool
ScaleWithDoc bool
ScaleWithDoc *bool
OddHeader string
OddFooter string
EvenHeader string
@ -1006,6 +1006,9 @@ type PageLayoutOptions struct {
FitToWidth *int
// BlackAndWhite specified print black and white.
BlackAndWhite *bool
// PageOrder specifies the ordering of multiple pages. Values
// accepted: overThenDown, downThenOver
PageOrder *string
}
// ViewOptions directly maps the settings of sheet view.