From 77779555773da879a8e04e5ba85da72d9f782a3c Mon Sep 17 00:00:00 2001 From: Adrien Loison Date: Tue, 2 May 2017 14:11:52 +0200 Subject: [PATCH] First commit to gh-pages branch --- .editorconfig | 15 - .gitattributes | 11 - .gitignore | 5 - .scrutinizer.yml | 57 - .travis.yml | 23 - CONTRIBUTING.md | 75 -- LICENSE | 166 --- README.md | 407 -------- composer.json | 37 - composer.lock | 980 ------------------ logo.png | Bin 34091 -> 0 bytes phpunit.xml | 25 - src/Spout/Autoloader/Psr4Autoloader.php | 150 --- src/Spout/Autoloader/autoload.php | 15 - src/Spout/Common/Escaper/CSV.php | 38 - src/Spout/Common/Escaper/EscaperInterface.php | 27 - src/Spout/Common/Escaper/ODS.php | 66 -- src/Spout/Common/Escaper/XLSX.php | 185 ---- .../Exception/EncodingConversionException.php | 13 - src/Spout/Common/Exception/IOException.php | 13 - .../Exception/InvalidArgumentException.php | 13 - src/Spout/Common/Exception/SpoutException.php | 13 - .../Exception/UnsupportedTypeException.php | 13 - src/Spout/Common/Helper/EncodingHelper.php | 175 ---- src/Spout/Common/Helper/FileSystemHelper.php | 133 --- .../Common/Helper/GlobalFunctionsHelper.php | 318 ------ src/Spout/Common/Helper/StringHelper.php | 71 -- src/Spout/Common/Singleton.php | 41 - src/Spout/Common/Type.php | 16 - src/Spout/Reader/AbstractReader.php | 238 ----- src/Spout/Reader/CSV/Reader.php | 149 --- src/Spout/Reader/CSV/ReaderOptions.php | 110 -- src/Spout/Reader/CSV/RowIterator.php | 260 ----- src/Spout/Reader/CSV/Sheet.php | 62 -- src/Spout/Reader/CSV/SheetIterator.php | 95 -- src/Spout/Reader/Common/ReaderOptions.php | 58 -- src/Spout/Reader/Common/XMLProcessor.php | 152 --- .../IteratorNotRewindableException.php | 13 - .../Exception/NoSheetsFoundException.php | 13 - .../Reader/Exception/ReaderException.php | 15 - .../Exception/ReaderNotOpenedException.php | 13 - .../SharedStringNotFoundException.php | 13 - .../Exception/XMLProcessingException.php | 12 - src/Spout/Reader/IteratorInterface.php | 18 - .../Reader/ODS/Helper/CellValueFormatter.php | 231 ----- .../Reader/ODS/Helper/SettingsHelper.php | 51 - src/Spout/Reader/ODS/Reader.php | 85 -- src/Spout/Reader/ODS/ReaderOptions.php | 14 - src/Spout/Reader/ODS/RowIterator.php | 352 ------- src/Spout/Reader/ODS/Sheet.php | 81 -- src/Spout/Reader/ODS/SheetIterator.php | 168 --- src/Spout/Reader/ReaderFactory.php | 48 - src/Spout/Reader/ReaderInterface.php | 36 - src/Spout/Reader/SheetInterface.php | 18 - .../Wrapper/XMLInternalErrorsHelper.php | 82 -- src/Spout/Reader/Wrapper/XMLReader.php | 167 --- src/Spout/Reader/XLSX/Helper/CellHelper.php | 106 -- .../Reader/XLSX/Helper/CellValueFormatter.php | 300 ------ .../Reader/XLSX/Helper/DateFormatHelper.php | 124 --- .../CachingStrategyFactory.php | 159 --- .../CachingStrategyInterface.php | 44 - .../FileBasedStrategy.php | 193 ---- .../SharedStringsCaching/InMemoryStrategy.php | 83 -- .../XLSX/Helper/SharedStringsHelper.php | 245 ----- src/Spout/Reader/XLSX/Helper/SheetHelper.php | 156 --- src/Spout/Reader/XLSX/Helper/StyleHelper.php | 330 ------ src/Spout/Reader/XLSX/Reader.php | 113 -- src/Spout/Reader/XLSX/ReaderOptions.php | 33 - src/Spout/Reader/XLSX/RowIterator.php | 406 -------- src/Spout/Reader/XLSX/Sheet.php | 79 -- src/Spout/Reader/XLSX/SheetIterator.php | 114 -- .../Writer/AbstractMultiSheetsWriter.php | 119 --- src/Spout/Writer/AbstractWriter.php | 384 ------- src/Spout/Writer/CSV/Writer.php | 118 --- .../Common/Helper/AbstractStyleHelper.php | 138 --- src/Spout/Writer/Common/Helper/CellHelper.php | 91 -- src/Spout/Writer/Common/Helper/ZipHelper.php | 217 ---- .../Common/Internal/AbstractWorkbook.php | 192 ---- .../Common/Internal/WorkbookInterface.php | 74 -- .../Common/Internal/WorksheetInterface.php | 40 - src/Spout/Writer/Common/Sheet.php | 183 ---- .../Exception/Border/InvalidNameException.php | 16 - .../Border/InvalidStyleException.php | 16 - .../Border/InvalidWidthException.php | 16 - .../Exception/InvalidColorException.php | 13 - .../Exception/InvalidSheetNameException.php | 13 - .../Exception/SheetNotFoundException.php | 13 - .../WriterAlreadyOpenedException.php | 13 - .../Writer/Exception/WriterException.php | 15 - .../Exception/WriterNotOpenedException.php | 13 - src/Spout/Writer/ODS/Helper/BorderHelper.php | 68 -- .../Writer/ODS/Helper/FileSystemHelper.php | 279 ----- src/Spout/Writer/ODS/Helper/StyleHelper.php | 356 ------- src/Spout/Writer/ODS/Internal/Workbook.php | 119 --- src/Spout/Writer/ODS/Internal/Worksheet.php | 233 ----- src/Spout/Writer/ODS/Writer.php | 93 -- src/Spout/Writer/Style/Border.php | 85 -- src/Spout/Writer/Style/BorderBuilder.php | 75 -- src/Spout/Writer/Style/BorderPart.php | 184 ---- src/Spout/Writer/Style/Color.php | 87 -- src/Spout/Writer/Style/Style.php | 426 -------- src/Spout/Writer/Style/StyleBuilder.php | 159 --- src/Spout/Writer/WriterFactory.php | 48 - src/Spout/Writer/WriterInterface.php | 91 -- src/Spout/Writer/XLSX/Helper/BorderHelper.php | 68 -- .../Writer/XLSX/Helper/FileSystemHelper.php | 371 ------- .../XLSX/Helper/SharedStringsHelper.php | 107 -- src/Spout/Writer/XLSX/Helper/StyleHelper.php | 344 ------ src/Spout/Writer/XLSX/Internal/Workbook.php | 135 --- src/Spout/Writer/XLSX/Internal/Worksheet.php | 275 ----- src/Spout/Writer/XLSX/Writer.php | 132 --- tests/Spout/Common/Escaper/ODSTest.php | 42 - tests/Spout/Common/Escaper/XLSXTest.php | 83 -- .../Common/Helper/EncodingHelperTest.php | 223 ---- .../Common/Helper/FileSystemHelperTest.php | 59 -- tests/Spout/Reader/CSV/ReaderTest.php | 518 --------- tests/Spout/Reader/CSV/SheetTest.php | 46 - tests/Spout/Reader/CSV/SpoutTestStream.php | 120 --- tests/Spout/Reader/ODS/ReaderTest.php | 549 ---------- tests/Spout/Reader/ODS/SheetTest.php | 66 -- tests/Spout/Reader/ReaderFactoryTest.php | 21 - tests/Spout/Reader/Wrapper/XMLReaderTest.php | 209 ---- .../Reader/XLSX/Helper/CellHelperTest.php | 71 -- .../XLSX/Helper/CellValueFormatterTest.php | 175 ---- .../XLSX/Helper/DateFormatHelperTest.php | 49 - .../CachingStrategyFactoryTest.php | 100 -- .../XLSX/Helper/SharedStringsHelperTest.php | 144 --- .../Reader/XLSX/Helper/StyleHelperTest.php | 179 ---- tests/Spout/Reader/XLSX/ReaderTest.php | 649 ------------ tests/Spout/Reader/XLSX/SheetTest.php | 54 - tests/Spout/ReflectionHelper.php | 115 -- tests/Spout/TestUsingResource.php | 141 --- tests/Spout/Writer/CSV/WriterTest.php | 214 ---- .../Writer/Common/Helper/CellHelperTest.php | 112 -- tests/Spout/Writer/Common/SheetTest.php | 111 -- .../Writer/ODS/Helper/StyleHelperTest.php | 89 -- tests/Spout/Writer/ODS/SheetTest.php | 134 --- tests/Spout/Writer/ODS/WriterTest.php | 603 ----------- .../Spout/Writer/ODS/WriterWithStyleTest.php | 494 --------- tests/Spout/Writer/Style/BorderTest.php | 110 -- tests/Spout/Writer/Style/ColorTest.php | 93 -- tests/Spout/Writer/Style/StyleTest.php | 160 --- tests/Spout/Writer/WriterFactoryTest.php | 21 - .../Writer/XLSX/Helper/StyleHelperTest.php | 113 -- tests/Spout/Writer/XLSX/SheetTest.php | 134 --- tests/Spout/Writer/XLSX/WriterTest.php | 611 ----------- .../Spout/Writer/XLSX/WriterWithStyleTest.php | 668 ------------ tests/bootstrap.php | 9 - tests/resources/csv/csv_all_lines_empty.csv | 2 - .../csv/csv_delimited_with_pipes.csv | 3 - tests/resources/csv/csv_empty.csv | 0 tests/resources/csv/csv_standard.csv | 3 - .../csv/csv_text_enclosed_with_pound.csv | 2 - tests/resources/csv/csv_with_CR_EOL.csv | 1 - .../resources/csv/csv_with_comma_enclosed.csv | 2 - .../csv/csv_with_different_cells_number.csv | 3 - tests/resources/csv/csv_with_empty_cells.csv | 3 - .../csv/csv_with_encoding_cp1252.csv | 3 - .../csv/csv_with_encoding_utf16le_no_bom.csv | Bin 142 -> 0 bytes tests/resources/csv/csv_with_line_breaks.csv | 2 - .../csv/csv_with_multiple_empty_lines.csv | 5 - tests/resources/csv/csv_with_utf16be_bom.csv | Bin 144 -> 0 bytes tests/resources/csv/csv_with_utf16le_bom.csv | Bin 144 -> 0 bytes tests/resources/csv/csv_with_utf32be_bom.csv | Bin 288 -> 0 bytes tests/resources/csv/csv_with_utf32le_bom.csv | Bin 288 -> 0 bytes tests/resources/csv/csv_with_utf8_bom.csv | 3 - .../resources/csv/sheet_with_empty_cells.csv | 3 - .../csv/sheet_with_untrimmed_strings.csv | 5 - .../resources/csv/sheet_with_zeros_in_row.csv | 3 - tests/resources/ods/attack_billion_laughs.ods | Bin 2681 -> 0 bytes .../resources/ods/attack_quadratic_blowup.ods | Bin 2682 -> 0 bytes tests/resources/ods/file_corrupted.ods | Bin 1735 -> 0 bytes .../file_generated_by_excel_2010_windows.ods | Bin 3281 -> 0 bytes .../file_generated_by_excel_office_online.ods | Bin 3054 -> 0 bytes .../ods/file_generated_by_libre_office.ods | Bin 9833 -> 0 bytes .../resources/ods/one_sheet_with_strings.ods | Bin 2561 -> 0 bytes .../ods/sheet_with_all_cell_types.ods | Bin 2774 -> 0 bytes .../ods/sheet_with_dates_and_times.ods | Bin 9988 -> 0 bytes .../resources/ods/sheet_with_empty_cells.ods | Bin 7473 -> 0 bytes tests/resources/ods/sheet_with_empty_rows.ods | Bin 9253 -> 0 bytes tests/resources/ods/sheet_with_formulas.ods | Bin 8450 -> 0 bytes tests/resources/ods/sheet_with_hyperlinks.ods | Bin 9905 -> 0 bytes .../ods/sheet_with_inline_font_formatting.ods | Bin 8552 -> 0 bytes .../ods/sheet_with_invalid_date_time.ods | Bin 2582 -> 0 bytes .../ods/sheet_with_multiline_string.ods | Bin 2549 -> 0 bytes tests/resources/ods/sheet_with_no_cells.ods | Bin 6799 -> 0 bytes .../sheet_with_number_columns_repeated.ods | Bin 2599 -> 0 bytes .../ods/sheet_with_number_rows_repeated.ods | Bin 2558 -> 0 bytes .../ods/sheet_with_only_one_cell.ods | Bin 2531 -> 0 bytes .../ods/sheet_with_undefined_value_type.ods | Bin 2552 -> 0 bytes .../ods/sheet_with_untrimmed_strings.ods | Bin 2454 -> 0 bytes .../ods/sheet_with_various_spaces.ods | Bin 2833 -> 0 bytes .../resources/ods/sheet_with_zeros_in_row.ods | Bin 8593 -> 0 bytes .../ods/two_sheets_with_custom_names.ods | Bin 7200 -> 0 bytes .../two_sheets_with_no_settings_xml_file.ods | Bin 2633 -> 0 bytes .../resources/ods/two_sheets_with_strings.ods | Bin 2633 -> 0 bytes .../resources/xlsx/attack_billion_laughs.xlsx | Bin 3911 -> 0 bytes .../xlsx/attack_quadratic_blowup.xlsx | Bin 3877 -> 0 bytes tests/resources/xlsx/file_corrupted.xlsx | 3 - .../file_with_no_sheets_in_workbook_xml.xlsx | Bin 3704 -> 0 bytes ..._sheet_xml_not_matching_content_types.xlsx | Bin 3757 -> 0 bytes .../xlsx/one_sheet_with_inline_strings.xlsx | Bin 3737 -> 0 bytes ...one_sheet_with_invalid_xml_characters.xlsx | Bin 3597 -> 0 bytes ..._sheet_with_pre_encoded_html_entities.xlsx | Bin 3201 -> 0 bytes ...e_sheet_with_shared_multiline_strings.xlsx | Bin 3789 -> 0 bytes .../xlsx/one_sheet_with_shared_strings.xlsx | Bin 3788 -> 0 bytes ...ining_text_and_hyperlink_in_same_cell.xlsx | Bin 5560 -> 0 bytes ...h_shared_strings_missing_unique_count.xlsx | Bin 3616 -> 0 bytes ...trings_missing_unique_count_and_count.xlsx | Bin 3609 -> 0 bytes .../xlsx/sheet_with_all_cell_types.xlsx | Bin 3799 -> 0 bytes ...te_formats_and_no_apply_number_format.xlsx | Bin 21882 -> 0 bytes .../xlsx/sheet_with_dates_and_times.xlsx | Bin 22509 -> 0 bytes ...et_with_different_numeric_value_dates.xlsx | Bin 30377 -> 0 bytes ...et_with_different_numeric_value_times.xlsx | Bin 28226 -> 0 bytes ...sheet_with_dimensions_and_empty_cells.xlsx | Bin 3678 -> 0 bytes .../xlsx/sheet_with_empty_cells.xlsx | Bin 4724 -> 0 bytes ...with_empty_rows_and_missing_row_index.xlsx | Bin 3685 -> 0 bytes .../xlsx/sheet_with_empty_shared_string.xlsx | Bin 3628 -> 0 bytes tests/resources/xlsx/sheet_with_formulas.xlsx | Bin 30451 -> 0 bytes .../sheet_with_lots_of_shared_strings.xlsx | Bin 45183 -> 0 bytes .../sheet_with_missing_cell_reference.xlsx | Bin 5015 -> 0 bytes tests/resources/xlsx/sheet_with_no_cells.xlsx | Bin 3543 -> 0 bytes .../sheet_with_no_shared_strings_file.xlsx | Bin 27377 -> 0 bytes .../xlsx/sheet_with_prefixed_xml_files.xlsx | Bin 7084 -> 0 bytes ...et_with_preserve_space_shared_strings.xlsx | Bin 3637 -> 0 bytes .../xlsx/sheet_with_pronunciation.xlsx | Bin 3725 -> 0 bytes ...eric_value_date_formatted_differently.xlsx | Bin 32960 -> 0 bytes .../sheet_with_untrimmed_inline_strings.xlsx | Bin 3223 -> 0 bytes .../xlsx/sheet_with_zeros_in_row.xlsx | Bin 4732 -> 0 bytes ...et_without_dimensions_and_empty_cells.xlsx | Bin 3662 -> 0 bytes ..._dimensions_but_spans_and_empty_cells.xlsx | Bin 3669 -> 0 bytes .../xlsx/two_sheets_with_custom_names.xlsx | Bin 4258 -> 0 bytes ...th_custom_names_and_custom_active_tab.xlsx | Bin 4331 -> 0 bytes .../xlsx/two_sheets_with_inline_strings.xlsx | Bin 4253 -> 0 bytes .../xlsx/two_sheets_with_shared_strings.xlsx | Bin 4340 -> 0 bytes ...th_sheets_definition_in_reverse_order.xlsx | Bin 4253 -> 0 bytes 236 files changed, 20894 deletions(-) delete mode 100644 .editorconfig delete mode 100644 .gitattributes delete mode 100644 .gitignore delete mode 100644 .scrutinizer.yml delete mode 100644 .travis.yml delete mode 100644 CONTRIBUTING.md delete mode 100644 LICENSE delete mode 100644 README.md delete mode 100644 composer.json delete mode 100644 composer.lock delete mode 100644 logo.png delete mode 100644 phpunit.xml delete mode 100644 src/Spout/Autoloader/Psr4Autoloader.php delete mode 100644 src/Spout/Autoloader/autoload.php delete mode 100644 src/Spout/Common/Escaper/CSV.php delete mode 100644 src/Spout/Common/Escaper/EscaperInterface.php delete mode 100644 src/Spout/Common/Escaper/ODS.php delete mode 100644 src/Spout/Common/Escaper/XLSX.php delete mode 100644 src/Spout/Common/Exception/EncodingConversionException.php delete mode 100644 src/Spout/Common/Exception/IOException.php delete mode 100644 src/Spout/Common/Exception/InvalidArgumentException.php delete mode 100644 src/Spout/Common/Exception/SpoutException.php delete mode 100644 src/Spout/Common/Exception/UnsupportedTypeException.php delete mode 100644 src/Spout/Common/Helper/EncodingHelper.php delete mode 100644 src/Spout/Common/Helper/FileSystemHelper.php delete mode 100644 src/Spout/Common/Helper/GlobalFunctionsHelper.php delete mode 100644 src/Spout/Common/Helper/StringHelper.php delete mode 100644 src/Spout/Common/Singleton.php delete mode 100644 src/Spout/Common/Type.php delete mode 100644 src/Spout/Reader/AbstractReader.php delete mode 100644 src/Spout/Reader/CSV/Reader.php delete mode 100644 src/Spout/Reader/CSV/ReaderOptions.php delete mode 100644 src/Spout/Reader/CSV/RowIterator.php delete mode 100644 src/Spout/Reader/CSV/Sheet.php delete mode 100644 src/Spout/Reader/CSV/SheetIterator.php delete mode 100644 src/Spout/Reader/Common/ReaderOptions.php delete mode 100644 src/Spout/Reader/Common/XMLProcessor.php delete mode 100644 src/Spout/Reader/Exception/IteratorNotRewindableException.php delete mode 100644 src/Spout/Reader/Exception/NoSheetsFoundException.php delete mode 100644 src/Spout/Reader/Exception/ReaderException.php delete mode 100644 src/Spout/Reader/Exception/ReaderNotOpenedException.php delete mode 100644 src/Spout/Reader/Exception/SharedStringNotFoundException.php delete mode 100644 src/Spout/Reader/Exception/XMLProcessingException.php delete mode 100644 src/Spout/Reader/IteratorInterface.php delete mode 100644 src/Spout/Reader/ODS/Helper/CellValueFormatter.php delete mode 100644 src/Spout/Reader/ODS/Helper/SettingsHelper.php delete mode 100644 src/Spout/Reader/ODS/Reader.php delete mode 100644 src/Spout/Reader/ODS/ReaderOptions.php delete mode 100644 src/Spout/Reader/ODS/RowIterator.php delete mode 100644 src/Spout/Reader/ODS/Sheet.php delete mode 100644 src/Spout/Reader/ODS/SheetIterator.php delete mode 100644 src/Spout/Reader/ReaderFactory.php delete mode 100644 src/Spout/Reader/ReaderInterface.php delete mode 100644 src/Spout/Reader/SheetInterface.php delete mode 100644 src/Spout/Reader/Wrapper/XMLInternalErrorsHelper.php delete mode 100644 src/Spout/Reader/Wrapper/XMLReader.php delete mode 100644 src/Spout/Reader/XLSX/Helper/CellHelper.php delete mode 100644 src/Spout/Reader/XLSX/Helper/CellValueFormatter.php delete mode 100644 src/Spout/Reader/XLSX/Helper/DateFormatHelper.php delete mode 100644 src/Spout/Reader/XLSX/Helper/SharedStringsCaching/CachingStrategyFactory.php delete mode 100644 src/Spout/Reader/XLSX/Helper/SharedStringsCaching/CachingStrategyInterface.php delete mode 100644 src/Spout/Reader/XLSX/Helper/SharedStringsCaching/FileBasedStrategy.php delete mode 100644 src/Spout/Reader/XLSX/Helper/SharedStringsCaching/InMemoryStrategy.php delete mode 100644 src/Spout/Reader/XLSX/Helper/SharedStringsHelper.php delete mode 100644 src/Spout/Reader/XLSX/Helper/SheetHelper.php delete mode 100644 src/Spout/Reader/XLSX/Helper/StyleHelper.php delete mode 100644 src/Spout/Reader/XLSX/Reader.php delete mode 100644 src/Spout/Reader/XLSX/ReaderOptions.php delete mode 100644 src/Spout/Reader/XLSX/RowIterator.php delete mode 100644 src/Spout/Reader/XLSX/Sheet.php delete mode 100644 src/Spout/Reader/XLSX/SheetIterator.php delete mode 100644 src/Spout/Writer/AbstractMultiSheetsWriter.php delete mode 100644 src/Spout/Writer/AbstractWriter.php delete mode 100644 src/Spout/Writer/CSV/Writer.php delete mode 100644 src/Spout/Writer/Common/Helper/AbstractStyleHelper.php delete mode 100644 src/Spout/Writer/Common/Helper/CellHelper.php delete mode 100644 src/Spout/Writer/Common/Helper/ZipHelper.php delete mode 100644 src/Spout/Writer/Common/Internal/AbstractWorkbook.php delete mode 100644 src/Spout/Writer/Common/Internal/WorkbookInterface.php delete mode 100644 src/Spout/Writer/Common/Internal/WorksheetInterface.php delete mode 100644 src/Spout/Writer/Common/Sheet.php delete mode 100644 src/Spout/Writer/Exception/Border/InvalidNameException.php delete mode 100644 src/Spout/Writer/Exception/Border/InvalidStyleException.php delete mode 100644 src/Spout/Writer/Exception/Border/InvalidWidthException.php delete mode 100644 src/Spout/Writer/Exception/InvalidColorException.php delete mode 100644 src/Spout/Writer/Exception/InvalidSheetNameException.php delete mode 100644 src/Spout/Writer/Exception/SheetNotFoundException.php delete mode 100644 src/Spout/Writer/Exception/WriterAlreadyOpenedException.php delete mode 100644 src/Spout/Writer/Exception/WriterException.php delete mode 100644 src/Spout/Writer/Exception/WriterNotOpenedException.php delete mode 100644 src/Spout/Writer/ODS/Helper/BorderHelper.php delete mode 100644 src/Spout/Writer/ODS/Helper/FileSystemHelper.php delete mode 100644 src/Spout/Writer/ODS/Helper/StyleHelper.php delete mode 100644 src/Spout/Writer/ODS/Internal/Workbook.php delete mode 100644 src/Spout/Writer/ODS/Internal/Worksheet.php delete mode 100644 src/Spout/Writer/ODS/Writer.php delete mode 100644 src/Spout/Writer/Style/Border.php delete mode 100644 src/Spout/Writer/Style/BorderBuilder.php delete mode 100644 src/Spout/Writer/Style/BorderPart.php delete mode 100644 src/Spout/Writer/Style/Color.php delete mode 100644 src/Spout/Writer/Style/Style.php delete mode 100644 src/Spout/Writer/Style/StyleBuilder.php delete mode 100644 src/Spout/Writer/WriterFactory.php delete mode 100644 src/Spout/Writer/WriterInterface.php delete mode 100644 src/Spout/Writer/XLSX/Helper/BorderHelper.php delete mode 100644 src/Spout/Writer/XLSX/Helper/FileSystemHelper.php delete mode 100644 src/Spout/Writer/XLSX/Helper/SharedStringsHelper.php delete mode 100644 src/Spout/Writer/XLSX/Helper/StyleHelper.php delete mode 100644 src/Spout/Writer/XLSX/Internal/Workbook.php delete mode 100644 src/Spout/Writer/XLSX/Internal/Worksheet.php delete mode 100644 src/Spout/Writer/XLSX/Writer.php delete mode 100644 tests/Spout/Common/Escaper/ODSTest.php delete mode 100644 tests/Spout/Common/Escaper/XLSXTest.php delete mode 100644 tests/Spout/Common/Helper/EncodingHelperTest.php delete mode 100644 tests/Spout/Common/Helper/FileSystemHelperTest.php delete mode 100644 tests/Spout/Reader/CSV/ReaderTest.php delete mode 100644 tests/Spout/Reader/CSV/SheetTest.php delete mode 100644 tests/Spout/Reader/CSV/SpoutTestStream.php delete mode 100644 tests/Spout/Reader/ODS/ReaderTest.php delete mode 100644 tests/Spout/Reader/ODS/SheetTest.php delete mode 100644 tests/Spout/Reader/ReaderFactoryTest.php delete mode 100644 tests/Spout/Reader/Wrapper/XMLReaderTest.php delete mode 100644 tests/Spout/Reader/XLSX/Helper/CellHelperTest.php delete mode 100644 tests/Spout/Reader/XLSX/Helper/CellValueFormatterTest.php delete mode 100644 tests/Spout/Reader/XLSX/Helper/DateFormatHelperTest.php delete mode 100644 tests/Spout/Reader/XLSX/Helper/SharedStringsCaching/CachingStrategyFactoryTest.php delete mode 100644 tests/Spout/Reader/XLSX/Helper/SharedStringsHelperTest.php delete mode 100644 tests/Spout/Reader/XLSX/Helper/StyleHelperTest.php delete mode 100644 tests/Spout/Reader/XLSX/ReaderTest.php delete mode 100644 tests/Spout/Reader/XLSX/SheetTest.php delete mode 100644 tests/Spout/ReflectionHelper.php delete mode 100644 tests/Spout/TestUsingResource.php delete mode 100644 tests/Spout/Writer/CSV/WriterTest.php delete mode 100644 tests/Spout/Writer/Common/Helper/CellHelperTest.php delete mode 100644 tests/Spout/Writer/Common/SheetTest.php delete mode 100644 tests/Spout/Writer/ODS/Helper/StyleHelperTest.php delete mode 100644 tests/Spout/Writer/ODS/SheetTest.php delete mode 100644 tests/Spout/Writer/ODS/WriterTest.php delete mode 100644 tests/Spout/Writer/ODS/WriterWithStyleTest.php delete mode 100644 tests/Spout/Writer/Style/BorderTest.php delete mode 100644 tests/Spout/Writer/Style/ColorTest.php delete mode 100644 tests/Spout/Writer/Style/StyleTest.php delete mode 100644 tests/Spout/Writer/WriterFactoryTest.php delete mode 100644 tests/Spout/Writer/XLSX/Helper/StyleHelperTest.php delete mode 100644 tests/Spout/Writer/XLSX/SheetTest.php delete mode 100644 tests/Spout/Writer/XLSX/WriterTest.php delete mode 100644 tests/Spout/Writer/XLSX/WriterWithStyleTest.php delete mode 100644 tests/bootstrap.php delete mode 100644 tests/resources/csv/csv_all_lines_empty.csv delete mode 100644 tests/resources/csv/csv_delimited_with_pipes.csv delete mode 100644 tests/resources/csv/csv_empty.csv delete mode 100644 tests/resources/csv/csv_standard.csv delete mode 100644 tests/resources/csv/csv_text_enclosed_with_pound.csv delete mode 100644 tests/resources/csv/csv_with_CR_EOL.csv delete mode 100644 tests/resources/csv/csv_with_comma_enclosed.csv delete mode 100644 tests/resources/csv/csv_with_different_cells_number.csv delete mode 100644 tests/resources/csv/csv_with_empty_cells.csv delete mode 100644 tests/resources/csv/csv_with_encoding_cp1252.csv delete mode 100644 tests/resources/csv/csv_with_encoding_utf16le_no_bom.csv delete mode 100644 tests/resources/csv/csv_with_line_breaks.csv delete mode 100644 tests/resources/csv/csv_with_multiple_empty_lines.csv delete mode 100644 tests/resources/csv/csv_with_utf16be_bom.csv delete mode 100644 tests/resources/csv/csv_with_utf16le_bom.csv delete mode 100644 tests/resources/csv/csv_with_utf32be_bom.csv delete mode 100644 tests/resources/csv/csv_with_utf32le_bom.csv delete mode 100644 tests/resources/csv/csv_with_utf8_bom.csv delete mode 100644 tests/resources/csv/sheet_with_empty_cells.csv delete mode 100644 tests/resources/csv/sheet_with_untrimmed_strings.csv delete mode 100644 tests/resources/csv/sheet_with_zeros_in_row.csv delete mode 100644 tests/resources/ods/attack_billion_laughs.ods delete mode 100644 tests/resources/ods/attack_quadratic_blowup.ods delete mode 100644 tests/resources/ods/file_corrupted.ods delete mode 100755 tests/resources/ods/file_generated_by_excel_2010_windows.ods delete mode 100644 tests/resources/ods/file_generated_by_excel_office_online.ods delete mode 100644 tests/resources/ods/file_generated_by_libre_office.ods delete mode 100644 tests/resources/ods/one_sheet_with_strings.ods delete mode 100644 tests/resources/ods/sheet_with_all_cell_types.ods delete mode 100644 tests/resources/ods/sheet_with_dates_and_times.ods delete mode 100644 tests/resources/ods/sheet_with_empty_cells.ods delete mode 100644 tests/resources/ods/sheet_with_empty_rows.ods delete mode 100644 tests/resources/ods/sheet_with_formulas.ods delete mode 100644 tests/resources/ods/sheet_with_hyperlinks.ods delete mode 100644 tests/resources/ods/sheet_with_inline_font_formatting.ods delete mode 100644 tests/resources/ods/sheet_with_invalid_date_time.ods delete mode 100644 tests/resources/ods/sheet_with_multiline_string.ods delete mode 100644 tests/resources/ods/sheet_with_no_cells.ods delete mode 100644 tests/resources/ods/sheet_with_number_columns_repeated.ods delete mode 100644 tests/resources/ods/sheet_with_number_rows_repeated.ods delete mode 100644 tests/resources/ods/sheet_with_only_one_cell.ods delete mode 100644 tests/resources/ods/sheet_with_undefined_value_type.ods delete mode 100644 tests/resources/ods/sheet_with_untrimmed_strings.ods delete mode 100644 tests/resources/ods/sheet_with_various_spaces.ods delete mode 100644 tests/resources/ods/sheet_with_zeros_in_row.ods delete mode 100644 tests/resources/ods/two_sheets_with_custom_names.ods delete mode 100644 tests/resources/ods/two_sheets_with_no_settings_xml_file.ods delete mode 100644 tests/resources/ods/two_sheets_with_strings.ods delete mode 100644 tests/resources/xlsx/attack_billion_laughs.xlsx delete mode 100644 tests/resources/xlsx/attack_quadratic_blowup.xlsx delete mode 100644 tests/resources/xlsx/file_corrupted.xlsx delete mode 100644 tests/resources/xlsx/file_with_no_sheets_in_workbook_xml.xlsx delete mode 100644 tests/resources/xlsx/file_with_sheet_xml_not_matching_content_types.xlsx delete mode 100644 tests/resources/xlsx/one_sheet_with_inline_strings.xlsx delete mode 100644 tests/resources/xlsx/one_sheet_with_invalid_xml_characters.xlsx delete mode 100644 tests/resources/xlsx/one_sheet_with_pre_encoded_html_entities.xlsx delete mode 100644 tests/resources/xlsx/one_sheet_with_shared_multiline_strings.xlsx delete mode 100644 tests/resources/xlsx/one_sheet_with_shared_strings.xlsx delete mode 100644 tests/resources/xlsx/one_sheet_with_shared_strings_containing_text_and_hyperlink_in_same_cell.xlsx delete mode 100644 tests/resources/xlsx/one_sheet_with_shared_strings_missing_unique_count.xlsx delete mode 100644 tests/resources/xlsx/one_sheet_with_shared_strings_missing_unique_count_and_count.xlsx delete mode 100644 tests/resources/xlsx/sheet_with_all_cell_types.xlsx delete mode 100644 tests/resources/xlsx/sheet_with_custom_date_formats_and_no_apply_number_format.xlsx delete mode 100644 tests/resources/xlsx/sheet_with_dates_and_times.xlsx delete mode 100644 tests/resources/xlsx/sheet_with_different_numeric_value_dates.xlsx delete mode 100644 tests/resources/xlsx/sheet_with_different_numeric_value_times.xlsx delete mode 100644 tests/resources/xlsx/sheet_with_dimensions_and_empty_cells.xlsx delete mode 100644 tests/resources/xlsx/sheet_with_empty_cells.xlsx delete mode 100644 tests/resources/xlsx/sheet_with_empty_rows_and_missing_row_index.xlsx delete mode 100644 tests/resources/xlsx/sheet_with_empty_shared_string.xlsx delete mode 100644 tests/resources/xlsx/sheet_with_formulas.xlsx delete mode 100644 tests/resources/xlsx/sheet_with_lots_of_shared_strings.xlsx delete mode 100644 tests/resources/xlsx/sheet_with_missing_cell_reference.xlsx delete mode 100644 tests/resources/xlsx/sheet_with_no_cells.xlsx delete mode 100644 tests/resources/xlsx/sheet_with_no_shared_strings_file.xlsx delete mode 100644 tests/resources/xlsx/sheet_with_prefixed_xml_files.xlsx delete mode 100644 tests/resources/xlsx/sheet_with_preserve_space_shared_strings.xlsx delete mode 100644 tests/resources/xlsx/sheet_with_pronunciation.xlsx delete mode 100644 tests/resources/xlsx/sheet_with_same_numeric_value_date_formatted_differently.xlsx delete mode 100644 tests/resources/xlsx/sheet_with_untrimmed_inline_strings.xlsx delete mode 100644 tests/resources/xlsx/sheet_with_zeros_in_row.xlsx delete mode 100644 tests/resources/xlsx/sheet_without_dimensions_and_empty_cells.xlsx delete mode 100644 tests/resources/xlsx/sheet_without_dimensions_but_spans_and_empty_cells.xlsx delete mode 100644 tests/resources/xlsx/two_sheets_with_custom_names.xlsx delete mode 100644 tests/resources/xlsx/two_sheets_with_custom_names_and_custom_active_tab.xlsx delete mode 100644 tests/resources/xlsx/two_sheets_with_inline_strings.xlsx delete mode 100644 tests/resources/xlsx/two_sheets_with_shared_strings.xlsx delete mode 100644 tests/resources/xlsx/two_sheets_with_sheets_definition_in_reverse_order.xlsx diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 1485c5e..0000000 --- a/.editorconfig +++ /dev/null @@ -1,15 +0,0 @@ -root = true - -[*] -charset = utf-8 -end_of_line = lf -indent_style = space -indent_size = 4 -trim_trailing_whitespace = true -insert_final_newline = true - -[{.travis.yml}] -indent_size = 2 - -[*.md] -trim_trailing_whitespace = false diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 5305c97..0000000 --- a/.gitattributes +++ /dev/null @@ -1,11 +0,0 @@ -# Ignore all test and documentation for archive -/tests export-ignore -/.editorconfig export-ignore -/.gitattributes export-ignore -/.gitignore export-ignore -/.scrutinizer.yml export-ignore -/.travis.yml export-ignore -/composer.lock export-ignore -/CONTRIBUTING.md export-ignore -/logo.png export-ignore -/phpunit.xml export-ignore diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 68715b4..0000000 --- a/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -/tests/resources/generated -/tests/coverage -/vendor -/.idea -*.iml diff --git a/.scrutinizer.yml b/.scrutinizer.yml deleted file mode 100644 index 42f2492..0000000 --- a/.scrutinizer.yml +++ /dev/null @@ -1,57 +0,0 @@ -filter: - excluded_paths: [vendor/*, tests/*] - -tools: - external_code_coverage: - timeout: 600 # Wait 10 minutes for results - runs: 3 # Merge results for 5.4, 5.5 and 5.6 jobs - php_mess_detector: true - php_code_sniffer: - config: - standard: PSR4 - filter: - paths: ['src'] - sensiolabs_security_checker: true - php_pdepend: true - php_loc: - enabled: true - filter: - paths: ['src'] - php_cpd: false # Must be disabled to use php_sim instead - php_sim: - enabled: true - filter: - paths: ['src'] - -build_failure_conditions: - - 'project.metric("scrutinizer.quality", < 9)' # Code Quality Rating drops below 9 - - 'project.metric_change("scrutinizer.test_coverage", < -0.005)' # Code Coverage decreased by more than 0.5% - - 'project.metric("scrutinizer.test_coverage", < 0.97)' # Code Coverage drops below 97% - -checks: - php: - remove_extra_empty_lines: true - remove_php_closing_tag: true - remove_trailing_whitespace: true - fix_use_statements: - remove_unused: true - preserve_multiple: false - preserve_blanklines: true - fix_php_opening_tag: true - fix_linefeed: true - fix_line_ending: true - fix_identation_4spaces: true - fix_doc_comments: true - uppercase_constants: true - use_self_instead_of_fqcn: true - simplify_boolean_return: true - return_doc_comments: true - return_doc_comment_if_not_inferrable: true - phpunit_assertions: true - parameters_in_camelcaps: true - parameter_doc_comments: true - param_doc_comment_if_not_inferrable: true - optional_parameters_at_the_end: true - newline_at_end_of_file: true - encourage_single_quotes: true - diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index b23c7fd..0000000 --- a/.travis.yml +++ /dev/null @@ -1,23 +0,0 @@ -language: php - -php: - - 5.4 - - 5.5 - - 5.6 - - 7.0 - - hhvm - -cache: - directories: - - $HOME/.composer/cache - -install: - - composer install --no-interaction --prefer-source - -script: - - mkdir -p build/logs - - php vendor/bin/phpunit --coverage-clover=build/logs/coverage.clover - -after_script: - - if [[ $TRAVIS_PHP_VERSION != 'hhvm' && $TRAVIS_PHP_VERSION != '7.0' ]]; then wget https://scrutinizer-ci.com/ocular.phar; fi - - if [[ $TRAVIS_PHP_VERSION != 'hhvm' && $TRAVIS_PHP_VERSION != '7.0' ]]; then php ocular.phar code-coverage:upload --format=php-clover build/logs/coverage.clover; fi diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 4cbf577..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,75 +0,0 @@ -# Contributing - -All contributions are welcome to this project. - -## Contributor License Agreement - -Before a contribution can be merged into this project, please fill out the Contributor License Agreement (CLA) located at: - -http://opensource.box.com/cla - -To learn more about CLAs and why they are important to open source projects, please see the [Wikipedia entry](http://en.wikipedia.org/wiki/Contributor_License_Agreement). - -## How to contribute - -* **File an issue** - if you found a bug, want to request an enhancement, or want to implement something (bug fix or feature). -* **Send a pull request** - if you want to contribute code. Please be sure to file an issue first. - -## Pull request best practices - -We want to accept your pull requests. Please follow these steps: - -### Step 1: File an issue - -Before writing any code, please file an issue stating the problem you want to solve or the feature you want to implement. This allows us to give you feedback before you spend any time writing code. There may be a known limitation that can't be addressed, or a bug that has already been fixed in a different way. The issue allows us to communicate and figure out if it's worth your time to write a bunch of code for the project. - -### Step 2: Fork this repository in GitHub - -This will create your own copy of our repository. - -### Step 3: Add the upstream source - -The upstream source is the project under the Box organization on GitHub. To add an upstream source for this project, type: - -``` -git remote add upstream git@github.com:box/spout.git -``` - -This will come in useful later. - -### Step 4: Create a feature branch - -Create a branch with a descriptive name, such as `add-search`. - -### Step 5: Push your feature branch to your fork - -As you develop code, continue to push code to your remote feature branch. Please make sure to include the issue number you're addressing in your commit message, such as: - -``` -git commit -m "Adding search (fixes #123)" -``` - -This helps us out by allowing us to track which issue your commit relates to. - -Keep a separate feature branch for each issue you want to address. - -### Step 6: Rebase - -Before sending a pull request, rebase against upstream, such as: - -``` -git fetch upstream -git rebase upstream/master -``` - -This will add your changes on top of what's already in upstream, minimizing merge issues. - -### Step 7: Run the tests - -Make sure that all tests are passing before submitting a pull request. - -### Step 8: Send the pull request - -Send the pull request from your feature branch to us. Be sure to include a description that lets us know what work you did. - -Keep in mind that we like to see one issue addressed per pull request, as this helps keep our git history clean and we can more easily track down issues. diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 167ec4d..0000000 --- a/LICENSE +++ /dev/null @@ -1,166 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS diff --git a/README.md b/README.md deleted file mode 100644 index a0fb2bf..0000000 --- a/README.md +++ /dev/null @@ -1,407 +0,0 @@ -# Spout - -[![Latest Stable Version](https://poser.pugx.org/box/spout/v/stable)](https://packagist.org/packages/box/spout) -[![Project Status](http://opensource.box.com/badges/active.svg)](http://opensource.box.com/badges) -[![Build Status](https://travis-ci.org/box/spout.svg?branch=master)](https://travis-ci.org/box/spout) -[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/box/spout/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/box/spout/?branch=master) -[![Code Coverage](https://scrutinizer-ci.com/g/box/spout/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/box/spout/?branch=master) -[![Total Downloads](https://poser.pugx.org/box/spout/downloads)](https://packagist.org/packages/box/spout) - -Spout is a PHP library to read and write spreadsheet files (CSV, XLSX and ODS), in a fast and scalable way. -Contrary to other file readers or writers, it is capable of processing very large files while keeping the memory usage really low (less than 3MB). - -Join the community and come discuss about Spout: [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/box/spout?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) - -## Installation - -### Composer (recommended) - -Spout can be installed directly from [Composer](https://getcomposer.org/). - -Run the following command: -``` -$ composer require box/spout -``` - -### Manual installation - -If you can't use Composer, no worries! You can still install Spout manually. - -> Before starting, make sure your system meets the [requirements](#requirements). - -1. Download the source code from the [Releases page](https://github.com/box/spout/releases) -2. Extract the downloaded content into your project. -3. Add this code to the top controller (index.php) or wherever it may be more appropriate: -```php -require_once '[PATH/TO]/src/Spout/Autoloader/autoload.php'; // don't forget to change the path! -``` - - -## Requirements - -* PHP version 5.4.0 or higher -* PHP extension `php_zip` enabled -* PHP extension `php_xmlreader` enabled - - -## Basic usage - -### Reader - -Regardless of the file type, the interface to read a file is always the same: - -```php -use Box\Spout\Reader\ReaderFactory; -use Box\Spout\Common\Type; - -$reader = ReaderFactory::create(Type::XLSX); // for XLSX files -//$reader = ReaderFactory::create(Type::CSV); // for CSV files -//$reader = ReaderFactory::create(Type::ODS); // for ODS files - -$reader->open($filePath); - -foreach ($reader->getSheetIterator() as $sheet) { - foreach ($sheet->getRowIterator() as $row) { - // do stuff with the row - } -} - -$reader->close(); -``` - -If there are multiple sheets in the file, the reader will read all of them sequentially. - -### Writer - -As with the reader, there is one common interface to write data to a file: - -```php -use Box\Spout\Writer\WriterFactory; -use Box\Spout\Common\Type; - -$writer = WriterFactory::create(Type::XLSX); // for XLSX files -//$writer = WriterFactory::create(Type::CSV); // for CSV files -//$writer = WriterFactory::create(Type::ODS); // for ODS files - -$writer->openToFile($filePath); // write data to a file or to a PHP stream -//$writer->openToBrowser($fileName); // stream data directly to the browser - -$writer->addRow($singleRow); // add a row at a time -$writer->addRows($multipleRows); // add multiple rows at a time - -$writer->close(); -``` - -For XLSX and ODS files, the number of rows per sheet is limited to 1,048,576. By default, once this limit is reached, the writer will automatically create a new sheet and continue writing data into it. - - -## Advanced usage - -If you are looking for how to perform some common, more advanced tasks with Spout, please take a look at the [Wiki](https://github.com/box/spout/wiki). It contains code snippets, ready to be used. - -### Configuring the CSV reader and writer - -It is possible to configure both the CSV reader and writer to specify the field separator as well as the field enclosure: -```php -use Box\Spout\Reader\ReaderFactory; -use Box\Spout\Common\Type; - -$reader = ReaderFactory::create(Type::CSV); -$reader->setFieldDelimiter('|'); -$reader->setFieldEnclosure('@'); -$reader->setEndOfLineCharacter("\r"); -``` - -Additionally, if you need to read non UTF-8 files, you can specify the encoding of your file this way: -```php -$reader->setEncoding('UTF-16LE'); -``` - -By default, the writer generates CSV files encoded in UTF-8, with a BOM. -It is however possible to not include the BOM: -```php -use Box\Spout\Writer\WriterFactory; -use Box\Spout\Common\Type; - -$writer = WriterFactory::create(Type::CSV); -$writer->setShouldAddBOM(false); -``` - - -### Configuring the XLSX and ODS readers and writers - -#### Row styling - -It is possible to apply some formatting options to a row. Spout supports fonts, background, borders as well as alignment styles. - -```php -use Box\Spout\Common\Type; -use Box\Spout\Writer\WriterFactory; -use Box\Spout\Writer\Style\StyleBuilder; -use Box\Spout\Writer\Style\Color; - -$style = (new StyleBuilder()) - ->setFontBold() - ->setFontSize(15) - ->setFontColor(Color::BLUE) - ->setShouldWrapText() - ->setBackgroundColor(Color::YELLOW) - ->build(); - -$writer = WriterFactory::create(Type::XLSX); -$writer->openToFile($filePath); - -$writer->addRowWithStyle($singleRow, $style); // style will only be applied to this row -$writer->addRow($otherSingleRow); // no style will be applied -$writer->addRowsWithStyle($multipleRows, $style); // style will be applied to all given rows - -$writer->close(); -``` - -Adding borders to a row requires a ```Border``` object. - -```php -use Box\Spout\Common\Type; -use Box\Spout\Writer\Style\Border; -use Box\Spout\Writer\Style\BorderBuilder; -use Box\Spout\Writer\Style\Color; -use Box\Spout\Writer\Style\StyleBuilder; -use Box\Spout\Writer\WriterFactory; - -$border = (new BorderBuilder()) - ->setBorderBottom(Color::GREEN, Border::WIDTH_THIN, Border::STYLE_DASHED) - ->build(); - -$style = (new StyleBuilder()) - ->setBorder($border) - ->build(); - -$writer = WriterFactory::create(Type::XLSX); -$writer->openToFile($filePath); - -$writer->addRowWithStyle(['Border Bottom Green Thin Dashed'], $style); - -$writer->close(); -``` - -Spout will use a default style for all created rows. This style can be overridden this way: - -```php -$defaultStyle = (new StyleBuilder()) - ->setFontName('Arial') - ->setFontSize(11) - ->build(); - -$writer = WriterFactory::create(Type::XLSX); -$writer->setDefaultRowStyle($defaultStyle) - ->openToFile($filePath); -``` - -Unfortunately, Spout does not support all the possible formatting options yet. But you can find the most important ones: - -| Category | Property | API -|-----------|---------------|--------------------------------------- -| Font | Bold | `StyleBuilder::setFontBold()` -| | Italic | `StyleBuilder::setFontItalic()` -| | Underline | `StyleBuilder::setFontUnderline()` -| | Strikethrough | `StyleBuilder::setFontStrikethrough()` -| | Font name | `StyleBuilder::setFontName('Arial')` -| | Font size | `StyleBuilder::setFontSize(14)` -| | Font color | `StyleBuilder::setFontColor(Color::BLUE)`
`StyleBuilder::setFontColor(Color::rgb(0, 128, 255))` -| Alignment | Wrap text | `StyleBuilder::setShouldWrapText(true|false)` - -#### New sheet creation - -It is also possible to change the behavior of the writer when the maximum number of rows (1,048,576) have been written in the current sheet: -```php -use Box\Spout\Writer\WriterFactory; -use Box\Spout\Common\Type; - -$writer = WriterFactory::create(Type::ODS); -$writer->setShouldCreateNewSheetsAutomatically(true); // default value -$writer->setShouldCreateNewSheetsAutomatically(false); // will stop writing new data when limit is reached -``` - -#### Using custom temporary folder - -Processing XLSX and ODS files require temporary files to be created. By default, Spout will use the system default temporary folder (as returned by `sys_get_temp_dir()`). It is possible to override this by explicitly setting it on the reader or writer: -```php -use Box\Spout\Writer\WriterFactory; -use Box\Spout\Common\Type; - -$writer = WriterFactory::create(Type::XLSX); -$writer->setTempFolder($customTempFolderPath); -``` - -#### Strings storage (XLSX writer) - -XLSX files support different ways to store the string values: -* Shared strings are meant to optimize file size by separating strings from the sheet representation and ignoring strings duplicates (if a string is used three times, only one string will be stored) -* Inline strings are less optimized (as duplicate strings are all stored) but is faster to process - -In order to keep the memory usage really low, Spout does not optimize strings when using shared strings. It is nevertheless possible to use this mode. -```php -use Box\Spout\Writer\WriterFactory; -use Box\Spout\Common\Type; - -$writer = WriterFactory::create(Type::XLSX); -$writer->setShouldUseInlineStrings(true); // default (and recommended) value -$writer->setShouldUseInlineStrings(false); // will use shared strings -``` - -> ##### Note on Apple Numbers and iOS support -> -> Apple's products (Numbers and the iOS previewer) don't support inline strings and display empty cells instead. Therefore, if these platforms need to be supported, make sure to use shared strings! - - -#### Date/Time formatting - -When reading a spreadsheet containing dates or times, Spout returns the values by default as DateTime objects. -It is possible to change this behavior and have a formatted date returned instead (e.g. "2016-11-29 1:22 AM"). The format of the date corresponds to what is specified in the spreadsheet. - -```php -use Box\Spout\Reader\ReaderFactory; -use Box\Spout\Common\Type; - -$reader = ReaderFactory::create(Type::XLSX); -$reader->setShouldFormatDates(false); // default value -$reader->setShouldFormatDates(true); // will return formatted dates -``` - -### Playing with sheets - -When creating a XLSX or ODS file, it is possible to control which sheet the data will be written into. At any time, you can retrieve or set the current sheet: -```php -$firstSheet = $writer->getCurrentSheet(); -$writer->addRow($rowForSheet1); // writes the row to the first sheet - -$newSheet = $writer->addNewSheetAndMakeItCurrent(); -$writer->addRow($rowForSheet2); // writes the row to the new sheet - -$writer->setCurrentSheet($firstSheet); -$writer->addRow($anotherRowForSheet1); // append the row to the first sheet -``` - -It is also possible to retrieve all the sheets currently created: -```php -$sheets = $writer->getSheets(); -``` - -If you rely on the sheet's name in your application, you can access it and customize it this way: -```php -// Accessing the sheet name when reading -foreach ($reader->getSheetIterator() as $sheet) { - $sheetName = $sheet->getName(); -} - -// Accessing the sheet name when writing -$sheet = $writer->getCurrentSheet(); -$sheetName = $sheet->getName(); - -// Customizing the sheet name when writing -$sheet = $writer->getCurrentSheet(); -$sheet->setName('My custom name'); -``` - -> Please note that Excel has some restrictions on the sheet's name: -> * it must not be blank -> * it must not exceed 31 characters -> * it must not contain these characters: \ / ? * : [ or ] -> * it must not start or end with a single quote -> * it must be unique -> -> Handling these restrictions is the developer's responsibility. Spout does not try to automatically change the sheet's name, as one may rely on this name to be exactly what was passed in. - -Finally, it is possible to know which sheet was active when the spreadsheet was last saved. This can be useful if you are only interested in processing the one sheet that was last focused. -```php -foreach ($reader->getSheetIterator() as $sheet) { - // only process data for the active sheet - if ($sheet->isActive()) { - // do something... - } -} -``` - -### Fluent interface - -Because fluent interfaces are great, you can use them with Spout: -```php -use Box\Spout\Writer\WriterFactory; -use Box\Spout\Common\Type; - -$writer = WriterFactory::create(Type::XLSX); -$writer->setTempFolder($customTempFolderPath) - ->setShouldUseInlineStrings(true) - ->openToFile($filePath) - ->addRow($headerRow) - ->addRows($dataRows) - ->close(); -``` - - -## Running tests - -On the `master` branch, only unit and functional tests are included. The performance tests require very large files and have been excluded. -If you just want to check that everything is working as expected, executing the tests of the `master` branch is enough. - -If you want to run performance tests, you will need to checkout the `perf-tests` branch. Multiple test suites can then be run, depending on the expected output: - -* `phpunit` - runs the whole test suite (unit + functional + performance tests) -* `phpunit --exclude-group perf-tests` - only runs the unit and functional tests -* `phpunit --group perf-tests` - only runs the performance tests - -For information, the performance tests take about 30 minutes to run (processing 1 million rows files is not a quick thing). - -> Performance tests status: [![Build Status](https://travis-ci.org/box/spout.svg?branch=perf-tests)](https://travis-ci.org/box/spout) - - -## Frequently Asked Questions - -#### How can Spout handle such large data sets and still use less than 3MB of memory? - -When writing data, Spout is streaming the data to files, one or few lines at a time. That means that it only keeps in memory the few rows that it needs to write. Once written, the memory is freed. - -Same goes with reading. Only one row at a time is stored in memory. A special technique is used to handle shared strings in XLSX, storing them - if needed - into several small temporary files that allows fast access. - -#### How long does it take to generate a file with X rows? - -Here are a few numbers regarding the performance of Spout: - -| Type | Action | 2,000 rows (6,000 cells) | 200,000 rows (600,000 cells) | 2,000,000 rows (6,000,000 cells) | -|------|-------------------------------|--------------------------|------------------------------|----------------------------------| -| CSV | Read | < 1 second | 4 seconds | 2-3 minutes | -| | Write | < 1 second | 2 seconds | 2-3 minutes | -| XLSX | Read
*inline strings* | < 1 second | 35-40 seconds | 18-20 minutes | -| | Read
*shared strings* | 1 second | 1-2 minutes | 35-40 minutes | -| | Write | 1 second | 20-25 seconds | 8-10 minutes | -| ODS | Read | 1 second | 1-2 minutes | 5-6 minutes | -| | Write | < 1 second | 35-40 seconds | 5-6 minutes | - -#### Does Spout support charts or formulas? - -No. This is a compromise to keep memory usage low. Charts and formulas requires data to be kept in memory in order to be used. -So the larger the file would be, the more memory would be consumed, preventing your code to scale well. - - -## Support - -Need to contact us directly? Email oss@box.com and be sure to include the name of this project in the subject. - -You can also ask questions, submit new features ideas or discuss about Spout in the chat room:
-[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/box/spout?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) - -## Copyright and License - -Copyright 2017 Box, Inc. All rights reserved. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/composer.json b/composer.json deleted file mode 100644 index 76485c9..0000000 --- a/composer.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "name": "box/spout", - "description": "PHP Library to read and write spreadsheet files (CSV, XLSX and ODS), in a fast and scalable way", - "type": "library", - "keywords": ["php","read","write","csv","xlsx","ods","odf","open","office","excel","spreadsheet","scale","memory","stream","ooxml"], - "license": "Apache-2.0", - "homepage": "https://www.github.com/box/spout", - "authors": [ - { - "name": "Adrien Loison", - "email": "adrien@box.com" - } - ], - "require": { - "php": ">=5.4.0", - "ext-zip": "*", - "ext-xmlreader" : "*" - }, - "require-dev": { - "phpunit/phpunit": "^4.8.0" - }, - "suggest": { - "ext-iconv": "To handle non UTF-8 CSV files (if \"php-intl\" is not already installed or is too limited)", - "ext-intl": "To handle non UTF-8 CSV files (if \"iconv\" is not already installed)" - }, - "autoload": { - "psr-4": { - "Box\\Spout\\": "src/Spout" - } - }, - "extra": { - "branch-alias": { - "dev-master": "2.8.x-dev" - } - } - -} diff --git a/composer.lock b/composer.lock deleted file mode 100644 index df7690a..0000000 --- a/composer.lock +++ /dev/null @@ -1,980 +0,0 @@ -{ - "_readme": [ - "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", - "This file is @generated automatically" - ], - "hash": "8957b9da742e28d7250c02fca8f9a5a7", - "content-hash": "973b8a4a1d8c520dd99fcd32cb5e022f", - "packages": [], - "packages-dev": [ - { - "name": "doctrine/instantiator", - "version": "1.0.5", - "source": { - "type": "git", - "url": "https://github.com/doctrine/instantiator.git", - "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d", - "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d", - "shasum": "" - }, - "require": { - "php": ">=5.3,<8.0-DEV" - }, - "require-dev": { - "athletic/athletic": "~0.1.8", - "ext-pdo": "*", - "ext-phar": "*", - "phpunit/phpunit": "~4.0", - "squizlabs/php_codesniffer": "~2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Marco Pivetta", - "email": "ocramius@gmail.com", - "homepage": "http://ocramius.github.com/" - } - ], - "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", - "homepage": "https://github.com/doctrine/instantiator", - "keywords": [ - "constructor", - "instantiate" - ], - "time": "2015-06-14 21:17:01" - }, - { - "name": "phpdocumentor/reflection-docblock", - "version": "2.0.4", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/d68dbdc53dc358a816f00b300704702b2eaff7b8", - "reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "require-dev": { - "phpunit/phpunit": "~4.0" - }, - "suggest": { - "dflydev/markdown": "~1.0", - "erusev/parsedown": "~1.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "psr-0": { - "phpDocumentor": [ - "src/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "mike.vanriel@naenius.com" - } - ], - "time": "2015-02-03 12:10:50" - }, - { - "name": "phpspec/prophecy", - "version": "v1.6.0", - "source": { - "type": "git", - "url": "https://github.com/phpspec/prophecy.git", - "reference": "3c91bdf81797d725b14cb62906f9a4ce44235972" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/3c91bdf81797d725b14cb62906f9a4ce44235972", - "reference": "3c91bdf81797d725b14cb62906f9a4ce44235972", - "shasum": "" - }, - "require": { - "doctrine/instantiator": "^1.0.2", - "php": "^5.3|^7.0", - "phpdocumentor/reflection-docblock": "~2.0", - "sebastian/comparator": "~1.1", - "sebastian/recursion-context": "~1.0" - }, - "require-dev": { - "phpspec/phpspec": "~2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.5.x-dev" - } - }, - "autoload": { - "psr-0": { - "Prophecy\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Konstantin Kudryashov", - "email": "ever.zet@gmail.com", - "homepage": "http://everzet.com" - }, - { - "name": "Marcello Duarte", - "email": "marcello.duarte@gmail.com" - } - ], - "description": "Highly opinionated mocking framework for PHP 5.3+", - "homepage": "https://github.com/phpspec/prophecy", - "keywords": [ - "Double", - "Dummy", - "fake", - "mock", - "spy", - "stub" - ], - "time": "2016-02-15 07:46:21" - }, - { - "name": "phpunit/php-code-coverage", - "version": "2.2.4", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/eabf68b476ac7d0f73793aada060f1c1a9bf8979", - "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979", - "shasum": "" - }, - "require": { - "php": ">=5.3.3", - "phpunit/php-file-iterator": "~1.3", - "phpunit/php-text-template": "~1.2", - "phpunit/php-token-stream": "~1.3", - "sebastian/environment": "^1.3.2", - "sebastian/version": "~1.0" - }, - "require-dev": { - "ext-xdebug": ">=2.1.4", - "phpunit/phpunit": "~4" - }, - "suggest": { - "ext-dom": "*", - "ext-xdebug": ">=2.2.1", - "ext-xmlwriter": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.2.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" - } - ], - "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", - "homepage": "https://github.com/sebastianbergmann/php-code-coverage", - "keywords": [ - "coverage", - "testing", - "xunit" - ], - "time": "2015-10-06 15:47:00" - }, - { - "name": "phpunit/php-file-iterator", - "version": "1.4.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "6150bf2c35d3fc379e50c7602b75caceaa39dbf0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/6150bf2c35d3fc379e50c7602b75caceaa39dbf0", - "reference": "6150bf2c35d3fc379e50c7602b75caceaa39dbf0", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.4.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" - } - ], - "description": "FilterIterator implementation that filters files based on a list of suffixes.", - "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", - "keywords": [ - "filesystem", - "iterator" - ], - "time": "2015-06-21 13:08:43" - }, - { - "name": "phpunit/php-text-template", - "version": "1.2.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", - "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Simple template engine.", - "homepage": "https://github.com/sebastianbergmann/php-text-template/", - "keywords": [ - "template" - ], - "time": "2015-06-21 13:50:34" - }, - { - "name": "phpunit/php-timer", - "version": "1.0.8", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "38e9124049cf1a164f1e4537caf19c99bf1eb260" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/38e9124049cf1a164f1e4537caf19c99bf1eb260", - "reference": "38e9124049cf1a164f1e4537caf19c99bf1eb260", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "require-dev": { - "phpunit/phpunit": "~4|~5" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" - } - ], - "description": "Utility class for timing", - "homepage": "https://github.com/sebastianbergmann/php-timer/", - "keywords": [ - "timer" - ], - "time": "2016-05-12 18:03:57" - }, - { - "name": "phpunit/php-token-stream", - "version": "1.4.8", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da", - "reference": "3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da", - "shasum": "" - }, - "require": { - "ext-tokenizer": "*", - "php": ">=5.3.3" - }, - "require-dev": { - "phpunit/phpunit": "~4.2" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.4-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Wrapper around PHP's tokenizer extension.", - "homepage": "https://github.com/sebastianbergmann/php-token-stream/", - "keywords": [ - "tokenizer" - ], - "time": "2015-09-15 10:49:45" - }, - { - "name": "phpunit/phpunit", - "version": "4.8.26", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "fc1d8cd5b5de11625979125c5639347896ac2c74" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/fc1d8cd5b5de11625979125c5639347896ac2c74", - "reference": "fc1d8cd5b5de11625979125c5639347896ac2c74", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-json": "*", - "ext-pcre": "*", - "ext-reflection": "*", - "ext-spl": "*", - "php": ">=5.3.3", - "phpspec/prophecy": "^1.3.1", - "phpunit/php-code-coverage": "~2.1", - "phpunit/php-file-iterator": "~1.4", - "phpunit/php-text-template": "~1.2", - "phpunit/php-timer": "^1.0.6", - "phpunit/phpunit-mock-objects": "~2.3", - "sebastian/comparator": "~1.1", - "sebastian/diff": "~1.2", - "sebastian/environment": "~1.3", - "sebastian/exporter": "~1.2", - "sebastian/global-state": "~1.0", - "sebastian/version": "~1.0", - "symfony/yaml": "~2.1|~3.0" - }, - "suggest": { - "phpunit/php-invoker": "~1.1" - }, - "bin": [ - "phpunit" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.8.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "The PHP Unit Testing framework.", - "homepage": "https://phpunit.de/", - "keywords": [ - "phpunit", - "testing", - "xunit" - ], - "time": "2016-05-17 03:09:28" - }, - { - "name": "phpunit/phpunit-mock-objects", - "version": "2.3.8", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/ac8e7a3db35738d56ee9a76e78a4e03d97628983", - "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983", - "shasum": "" - }, - "require": { - "doctrine/instantiator": "^1.0.2", - "php": ">=5.3.3", - "phpunit/php-text-template": "~1.2", - "sebastian/exporter": "~1.2" - }, - "require-dev": { - "phpunit/phpunit": "~4.4" - }, - "suggest": { - "ext-soap": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.3.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" - } - ], - "description": "Mock Object library for PHPUnit", - "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", - "keywords": [ - "mock", - "xunit" - ], - "time": "2015-10-02 06:51:40" - }, - { - "name": "sebastian/comparator", - "version": "1.2.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "937efb279bd37a375bcadf584dec0726f84dbf22" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/937efb279bd37a375bcadf584dec0726f84dbf22", - "reference": "937efb279bd37a375bcadf584dec0726f84dbf22", - "shasum": "" - }, - "require": { - "php": ">=5.3.3", - "sebastian/diff": "~1.2", - "sebastian/exporter": "~1.2" - }, - "require-dev": { - "phpunit/phpunit": "~4.4" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.2.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Volker Dusch", - "email": "github@wallbash.com" - }, - { - "name": "Bernhard Schussek", - "email": "bschussek@2bepublished.at" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides the functionality to compare PHP values for equality", - "homepage": "http://www.github.com/sebastianbergmann/comparator", - "keywords": [ - "comparator", - "compare", - "equality" - ], - "time": "2015-07-26 15:48:44" - }, - { - "name": "sebastian/diff", - "version": "1.4.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/13edfd8706462032c2f52b4b862974dd46b71c9e", - "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "require-dev": { - "phpunit/phpunit": "~4.8" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.4-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Kore Nordmann", - "email": "mail@kore-nordmann.de" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Diff implementation", - "homepage": "https://github.com/sebastianbergmann/diff", - "keywords": [ - "diff" - ], - "time": "2015-12-08 07:14:41" - }, - { - "name": "sebastian/environment", - "version": "1.3.7", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "4e8f0da10ac5802913afc151413bc8c53b6c2716" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/4e8f0da10ac5802913afc151413bc8c53b6c2716", - "reference": "4e8f0da10ac5802913afc151413bc8c53b6c2716", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "require-dev": { - "phpunit/phpunit": "~4.4" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.3.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides functionality to handle HHVM/PHP environments", - "homepage": "http://www.github.com/sebastianbergmann/environment", - "keywords": [ - "Xdebug", - "environment", - "hhvm" - ], - "time": "2016-05-17 03:18:57" - }, - { - "name": "sebastian/exporter", - "version": "1.2.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "7ae5513327cb536431847bcc0c10edba2701064e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/7ae5513327cb536431847bcc0c10edba2701064e", - "reference": "7ae5513327cb536431847bcc0c10edba2701064e", - "shasum": "" - }, - "require": { - "php": ">=5.3.3", - "sebastian/recursion-context": "~1.0" - }, - "require-dev": { - "phpunit/phpunit": "~4.4" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.2.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Volker Dusch", - "email": "github@wallbash.com" - }, - { - "name": "Bernhard Schussek", - "email": "bschussek@2bepublished.at" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Adam Harvey", - "email": "aharvey@php.net" - } - ], - "description": "Provides the functionality to export PHP variables for visualization", - "homepage": "http://www.github.com/sebastianbergmann/exporter", - "keywords": [ - "export", - "exporter" - ], - "time": "2015-06-21 07:55:53" - }, - { - "name": "sebastian/global-state", - "version": "1.1.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bc37d50fea7d017d3d340f230811c9f1d7280af4", - "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "require-dev": { - "phpunit/phpunit": "~4.2" - }, - "suggest": { - "ext-uopz": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Snapshotting of global state", - "homepage": "http://www.github.com/sebastianbergmann/global-state", - "keywords": [ - "global state" - ], - "time": "2015-10-12 03:26:01" - }, - { - "name": "sebastian/recursion-context", - "version": "1.0.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "913401df809e99e4f47b27cdd781f4a258d58791" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/913401df809e99e4f47b27cdd781f4a258d58791", - "reference": "913401df809e99e4f47b27cdd781f4a258d58791", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "require-dev": { - "phpunit/phpunit": "~4.4" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Adam Harvey", - "email": "aharvey@php.net" - } - ], - "description": "Provides functionality to recursively process PHP variables", - "homepage": "http://www.github.com/sebastianbergmann/recursion-context", - "time": "2015-11-11 19:50:13" - }, - { - "name": "sebastian/version", - "version": "1.0.6", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/version.git", - "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/58b3a85e7999757d6ad81c787a1fbf5ff6c628c6", - "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6", - "shasum": "" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library that helps with managing the version number of Git-hosted PHP projects", - "homepage": "https://github.com/sebastianbergmann/version", - "time": "2015-06-21 13:59:46" - }, - { - "name": "symfony/yaml", - "version": "v2.8.6", - "source": { - "type": "git", - "url": "https://github.com/symfony/yaml.git", - "reference": "e4fbcc65f90909c999ac3b4dfa699ee6563a9940" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/e4fbcc65f90909c999ac3b4dfa699ee6563a9940", - "reference": "e4fbcc65f90909c999ac3b4dfa699ee6563a9940", - "shasum": "" - }, - "require": { - "php": ">=5.3.9" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.8-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Yaml\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Yaml Component", - "homepage": "https://symfony.com", - "time": "2016-03-29 19:00:15" - } - ], - "aliases": [], - "minimum-stability": "stable", - "stability-flags": [], - "prefer-stable": false, - "prefer-lowest": false, - "platform": { - "php": ">=5.4.0", - "ext-zip": "*", - "ext-xmlreader": "*", - "ext-simplexml": "*" - }, - "platform-dev": [] -} diff --git a/logo.png b/logo.png deleted file mode 100644 index 1319250177173b1fd90f77dd2b8c0fb867642f13..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 34091 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 RgaiKFm46_Yf5+tM{{hUz;0^!) diff --git a/phpunit.xml b/phpunit.xml deleted file mode 100644 index 06ddf63..0000000 --- a/phpunit.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - tests/ - - - - - - src/ - - src/Spout/Autoloader - - - - - diff --git a/src/Spout/Autoloader/Psr4Autoloader.php b/src/Spout/Autoloader/Psr4Autoloader.php deleted file mode 100644 index 67cccc3..0000000 --- a/src/Spout/Autoloader/Psr4Autoloader.php +++ /dev/null @@ -1,150 +0,0 @@ -prefixes[$prefix]) === false) { - $this->prefixes[$prefix] = array(); - } - - // retain the base directory for the namespace prefix - if ($prepend) { - array_unshift($this->prefixes[$prefix], $baseDir); - } else { - array_push($this->prefixes[$prefix], $baseDir); - } - } - - /** - * Loads the class file for a given class name. - * - * @param string $class The fully-qualified class name. - * @return mixed The mapped file name on success, or boolean false on - * failure. - */ - public function loadClass($class) - { - // the current namespace prefix - $prefix = $class; - - // work backwards through the namespace names of the fully-qualified - // class name to find a mapped file name - while (false !== $pos = strrpos($prefix, '\\')) { - - // retain the trailing namespace separator in the prefix - $prefix = substr($class, 0, $pos + 1); - - // the rest is the relative class name - $relativeClass = substr($class, $pos + 1); - - // try to load a mapped file for the prefix and relative class - $mappedFile = $this->loadMappedFile($prefix, $relativeClass); - if ($mappedFile !== false) { - return $mappedFile; - } - - // remove the trailing namespace separator for the next iteration - // of strrpos() - $prefix = rtrim($prefix, '\\'); - } - - // never found a mapped file - return false; - } - - /** - * Load the mapped file for a namespace prefix and relative class. - * - * @param string $prefix The namespace prefix. - * @param string $relativeClass The relative class name. - * @return mixed Boolean false if no mapped file can be loaded, or the - * name of the mapped file that was loaded. - */ - protected function loadMappedFile($prefix, $relativeClass) - { - // are there any base directories for this namespace prefix? - if (isset($this->prefixes[$prefix]) === false) { - return false; - } - - // look through base directories for this namespace prefix - foreach ($this->prefixes[$prefix] as $baseDir) { - - // replace the namespace prefix with the base directory, - // replace namespace separators with directory separators - // in the relative class name, append with .php - $file = $baseDir - . str_replace('\\', '/', $relativeClass) - . '.php'; - - // if the mapped file exists, require it - if ($this->requireFile($file)) { - // yes, we're done - return $file; - } - } - - // never found it - return false; - } - - /** - * If a file exists, require it from the file system. - * - * @param string $file The file to require. - * @return bool True if the file exists, false if not. - */ - protected function requireFile($file) - { - if (file_exists($file)) { - require $file; - return true; - } - return false; - } -} diff --git a/src/Spout/Autoloader/autoload.php b/src/Spout/Autoloader/autoload.php deleted file mode 100644 index 73ee519..0000000 --- a/src/Spout/Autoloader/autoload.php +++ /dev/null @@ -1,15 +0,0 @@ -register(); -$loader->addNamespace('Box\Spout', $srcBaseDirectory); diff --git a/src/Spout/Common/Escaper/CSV.php b/src/Spout/Common/Escaper/CSV.php deleted file mode 100644 index 4bc2d1a..0000000 --- a/src/Spout/Common/Escaper/CSV.php +++ /dev/null @@ -1,38 +0,0 @@ -', '&') need to be encoded. - // Single and double quotes can be left as is. - $escapedString = htmlspecialchars($string, ENT_NOQUOTES); - - // control characters values are from 0 to 1F (hex values) in the ASCII table - // some characters should not be escaped though: "\t", "\r" and "\n". - $regexPattern = '[\x00-\x08' . - // skipping "\t" (0x9) and "\n" (0xA) - '\x0B-\x0C' . - // skipping "\r" (0xD) - '\x0E-\x1F]'; - $replacedString = preg_replace("/$regexPattern/", '�', $escapedString); - } - - return $replacedString; - } - - /** - * Unescapes the given string to make it compatible with XLSX - * - * @param string $string The string to unescape - * @return string The unescaped string - */ - public function unescape($string) - { - // ============== - // = WARNING = - // ============== - // It is assumed that the given string has already had its XML entities decoded. - // This is true if the string is coming from a DOMNode (as DOMNode already decode XML entities on creation). - // Therefore there is no need to call "htmlspecialchars_decode()". - return $string; - } -} diff --git a/src/Spout/Common/Escaper/XLSX.php b/src/Spout/Common/Escaper/XLSX.php deleted file mode 100644 index 7c6c61b..0000000 --- a/src/Spout/Common/Escaper/XLSX.php +++ /dev/null @@ -1,185 +0,0 @@ -escapableControlCharactersPattern = $this->getEscapableControlCharactersPattern(); - $this->controlCharactersEscapingMap = $this->getControlCharactersEscapingMap(); - $this->controlCharactersEscapingReverseMap = array_flip($this->controlCharactersEscapingMap); - } - - /** - * Escapes the given string to make it compatible with XLSX - * - * @param string $string The string to escape - * @return string The escaped string - */ - public function escape($string) - { - $escapedString = $this->escapeControlCharacters($string); - // @NOTE: Using ENT_NOQUOTES as only XML entities ('<', '>', '&') need to be encoded. - // Single and double quotes can be left as is. - $escapedString = htmlspecialchars($escapedString, ENT_NOQUOTES); - - return $escapedString; - } - - /** - * Unescapes the given string to make it compatible with XLSX - * - * @param string $string The string to unescape - * @return string The unescaped string - */ - public function unescape($string) - { - // ============== - // = WARNING = - // ============== - // It is assumed that the given string has already had its XML entities decoded. - // This is true if the string is coming from a DOMNode (as DOMNode already decode XML entities on creation). - // Therefore there is no need to call "htmlspecialchars_decode()". - $unescapedString = $this->unescapeControlCharacters($string); - - return $unescapedString; - } - - /** - * @return string Regex pattern containing all escapable control characters - */ - protected function getEscapableControlCharactersPattern() - { - // control characters values are from 0 to 1F (hex values) in the ASCII table - // some characters should not be escaped though: "\t", "\r" and "\n". - return '[\x00-\x08' . - // skipping "\t" (0x9) and "\n" (0xA) - '\x0B-\x0C' . - // skipping "\r" (0xD) - '\x0E-\x1F]'; - } - - /** - * Builds the map containing control characters to be escaped - * mapped to their escaped values. - * "\t", "\r" and "\n" don't need to be escaped. - * - * NOTE: the logic has been adapted from the XlsxWriter library (BSD License) - * @link https://github.com/jmcnamara/XlsxWriter/blob/f1e610f29/xlsxwriter/sharedstrings.py#L89 - * - * @return string[] - */ - protected function getControlCharactersEscapingMap() - { - $controlCharactersEscapingMap = []; - - // control characters values are from 0 to 1F (hex values) in the ASCII table - for ($charValue = 0x00; $charValue <= 0x1F; $charValue++) { - $character = chr($charValue); - if (preg_match("/{$this->escapableControlCharactersPattern}/", $character)) { - $charHexValue = dechex($charValue); - $escapedChar = '_x' . sprintf('%04s' , strtoupper($charHexValue)) . '_'; - $controlCharactersEscapingMap[$escapedChar] = $character; - } - } - - return $controlCharactersEscapingMap; - } - - /** - * Converts PHP control characters from the given string to OpenXML escaped control characters - * - * Excel escapes control characters with _xHHHH_ and also escapes any - * literal strings of that type by encoding the leading underscore. - * So "\0" -> _x0000_ and "_x0000_" -> _x005F_x0000_. - * - * NOTE: the logic has been adapted from the XlsxWriter library (BSD License) - * @link https://github.com/jmcnamara/XlsxWriter/blob/f1e610f29/xlsxwriter/sharedstrings.py#L89 - * - * @param string $string String to escape - * @return string - */ - protected function escapeControlCharacters($string) - { - $escapedString = $this->escapeEscapeCharacter($string); - - // if no control characters - if (!preg_match("/{$this->escapableControlCharactersPattern}/", $escapedString)) { - return $escapedString; - } - - return preg_replace_callback("/({$this->escapableControlCharactersPattern})/", function($matches) { - return $this->controlCharactersEscapingReverseMap[$matches[0]]; - }, $escapedString); - } - - /** - * Escapes the escape character: "_x0000_" -> "_x005F_x0000_" - * - * @param string $string String to escape - * @return string The escaped string - */ - protected function escapeEscapeCharacter($string) - { - return preg_replace('/_(x[\dA-F]{4})_/', '_x005F_$1_', $string); - } - - /** - * Converts OpenXML escaped control characters from the given string to PHP control characters - * - * Excel escapes control characters with _xHHHH_ and also escapes any - * literal strings of that type by encoding the leading underscore. - * So "_x0000_" -> "\0" and "_x005F_x0000_" -> "_x0000_" - * - * NOTE: the logic has been adapted from the XlsxWriter library (BSD License) - * @link https://github.com/jmcnamara/XlsxWriter/blob/f1e610f29/xlsxwriter/sharedstrings.py#L89 - * - * @param string $string String to unescape - * @return string - */ - protected function unescapeControlCharacters($string) - { - $unescapedString = $string; - - foreach ($this->controlCharactersEscapingMap as $escapedCharValue => $charValue) { - // only unescape characters that don't contain the escaped escape character for now - $unescapedString = preg_replace("/(?unescapeEscapeCharacter($unescapedString); - } - - /** - * Unecapes the escape character: "_x005F_x0000_" => "_x0000_" - * - * @param string $string String to unescape - * @return string The unescaped string - */ - protected function unescapeEscapeCharacter($string) - { - return preg_replace('/_x005F(_x[\dA-F]{4}_)/', '$1', $string); - } -} diff --git a/src/Spout/Common/Exception/EncodingConversionException.php b/src/Spout/Common/Exception/EncodingConversionException.php deleted file mode 100644 index 6dcc0a8..0000000 --- a/src/Spout/Common/Exception/EncodingConversionException.php +++ /dev/null @@ -1,13 +0,0 @@ -globalFunctionsHelper = $globalFunctionsHelper; - - $this->supportedEncodingsWithBom = [ - self::ENCODING_UTF8 => self::BOM_UTF8, - self::ENCODING_UTF16_LE => self::BOM_UTF16_LE, - self::ENCODING_UTF16_BE => self::BOM_UTF16_BE, - self::ENCODING_UTF32_LE => self::BOM_UTF32_LE, - self::ENCODING_UTF32_BE => self::BOM_UTF32_BE, - ]; - } - - /** - * Returns the number of bytes to use as offset in order to skip the BOM. - * - * @param resource $filePointer Pointer to the file to check - * @param string $encoding Encoding of the file to check - * @return int Bytes offset to apply to skip the BOM (0 means no BOM) - */ - public function getBytesOffsetToSkipBOM($filePointer, $encoding) - { - $byteOffsetToSkipBom = 0; - - if ($this->hasBOM($filePointer, $encoding)) { - $bomUsed = $this->supportedEncodingsWithBom[$encoding]; - - // we skip the N first bytes - $byteOffsetToSkipBom = strlen($bomUsed); - } - - return $byteOffsetToSkipBom; - } - - /** - * Returns whether the file identified by the given pointer has a BOM. - * - * @param resource $filePointer Pointer to the file to check - * @param string $encoding Encoding of the file to check - * @return bool TRUE if the file has a BOM, FALSE otherwise - */ - protected function hasBOM($filePointer, $encoding) - { - $hasBOM = false; - - $this->globalFunctionsHelper->rewind($filePointer); - - if (array_key_exists($encoding, $this->supportedEncodingsWithBom)) { - $potentialBom = $this->supportedEncodingsWithBom[$encoding]; - $numBytesInBom = strlen($potentialBom); - - $hasBOM = ($this->globalFunctionsHelper->fgets($filePointer, $numBytesInBom + 1) === $potentialBom); - } - - return $hasBOM; - } - - /** - * Attempts to convert a non UTF-8 string into UTF-8. - * - * @param string $string Non UTF-8 string to be converted - * @param string $sourceEncoding The encoding used to encode the source string - * @return string The converted, UTF-8 string - * @throws \Box\Spout\Common\Exception\EncodingConversionException If conversion is not supported or if the conversion failed - */ - public function attemptConversionToUTF8($string, $sourceEncoding) - { - return $this->attemptConversion($string, $sourceEncoding, self::ENCODING_UTF8); - } - - /** - * Attempts to convert a UTF-8 string into the given encoding. - * - * @param string $string UTF-8 string to be converted - * @param string $targetEncoding The encoding the string should be re-encoded into - * @return string The converted string, encoded with the given encoding - * @throws \Box\Spout\Common\Exception\EncodingConversionException If conversion is not supported or if the conversion failed - */ - public function attemptConversionFromUTF8($string, $targetEncoding) - { - return $this->attemptConversion($string, self::ENCODING_UTF8, $targetEncoding); - } - - /** - * Attempts to convert the given string to the given encoding. - * Depending on what is installed on the server, we will try to iconv or mbstring. - * - * @param string $string string to be converted - * @param string $sourceEncoding The encoding used to encode the source string - * @param string $targetEncoding The encoding the string should be re-encoded into - * @return string The converted string, encoded with the given encoding - * @throws \Box\Spout\Common\Exception\EncodingConversionException If conversion is not supported or if the conversion failed - */ - protected function attemptConversion($string, $sourceEncoding, $targetEncoding) - { - // if source and target encodings are the same, it's a no-op - if ($sourceEncoding === $targetEncoding) { - return $string; - } - - $convertedString = null; - - if ($this->canUseIconv()) { - $convertedString = $this->globalFunctionsHelper->iconv($string, $sourceEncoding, $targetEncoding); - } else if ($this->canUseMbString()) { - $convertedString = $this->globalFunctionsHelper->mb_convert_encoding($string, $sourceEncoding, $targetEncoding); - } else { - throw new EncodingConversionException("The conversion from $sourceEncoding to $targetEncoding is not supported. Please install \"iconv\" or \"PHP Intl\"."); - } - - if ($convertedString === false) { - throw new EncodingConversionException("The conversion from $sourceEncoding to $targetEncoding failed."); - } - - return $convertedString; - } - - /** - * Returns whether "iconv" can be used. - * - * @return bool TRUE if "iconv" is available and can be used, FALSE otherwise - */ - protected function canUseIconv() - { - return $this->globalFunctionsHelper->function_exists('iconv'); - } - - /** - * Returns whether "mb_string" functions can be used. - * These functions come with the PHP Intl package. - * - * @return bool TRUE if "mb_string" functions are available and can be used, FALSE otherwise - */ - protected function canUseMbString() - { - return $this->globalFunctionsHelper->function_exists('mb_convert_encoding'); - } -} diff --git a/src/Spout/Common/Helper/FileSystemHelper.php b/src/Spout/Common/Helper/FileSystemHelper.php deleted file mode 100644 index 3145be7..0000000 --- a/src/Spout/Common/Helper/FileSystemHelper.php +++ /dev/null @@ -1,133 +0,0 @@ -baseFolderRealPath = realpath($baseFolderPath); - } - - /** - * Creates an empty folder with the given name under the given parent folder. - * - * @param string $parentFolderPath The parent folder path under which the folder is going to be created - * @param string $folderName The name of the folder to create - * @return string Path of the created folder - * @throws \Box\Spout\Common\Exception\IOException If unable to create the folder or if the folder path is not inside of the base folder - */ - public function createFolder($parentFolderPath, $folderName) - { - $this->throwIfOperationNotInBaseFolder($parentFolderPath); - - $folderPath = $parentFolderPath . '/' . $folderName; - - $wasCreationSuccessful = mkdir($folderPath, 0777, true); - if (!$wasCreationSuccessful) { - throw new IOException("Unable to create folder: $folderPath"); - } - - return $folderPath; - } - - /** - * Creates a file with the given name and content in the given folder. - * The parent folder must exist. - * - * @param string $parentFolderPath The parent folder path where the file is going to be created - * @param string $fileName The name of the file to create - * @param string $fileContents The contents of the file to create - * @return string Path of the created file - * @throws \Box\Spout\Common\Exception\IOException If unable to create the file or if the file path is not inside of the base folder - */ - public function createFileWithContents($parentFolderPath, $fileName, $fileContents) - { - $this->throwIfOperationNotInBaseFolder($parentFolderPath); - - $filePath = $parentFolderPath . '/' . $fileName; - - $wasCreationSuccessful = file_put_contents($filePath, $fileContents); - if ($wasCreationSuccessful === false) { - throw new IOException("Unable to create file: $filePath"); - } - - return $filePath; - } - - /** - * Delete the file at the given path - * - * @param string $filePath Path of the file to delete - * @return void - * @throws \Box\Spout\Common\Exception\IOException If the file path is not inside of the base folder - */ - public function deleteFile($filePath) - { - $this->throwIfOperationNotInBaseFolder($filePath); - - if (file_exists($filePath) && is_file($filePath)) { - unlink($filePath); - } - } - - /** - * Delete the folder at the given path as well as all its contents - * - * @param string $folderPath Path of the folder to delete - * @return void - * @throws \Box\Spout\Common\Exception\IOException If the folder path is not inside of the base folder - */ - public function deleteFolderRecursively($folderPath) - { - $this->throwIfOperationNotInBaseFolder($folderPath); - - $itemIterator = new \RecursiveIteratorIterator( - new \RecursiveDirectoryIterator($folderPath, \RecursiveDirectoryIterator::SKIP_DOTS), - \RecursiveIteratorIterator::CHILD_FIRST - ); - - foreach ($itemIterator as $item) { - if ($item->isDir()) { - rmdir($item->getPathname()); - } else { - unlink($item->getPathname()); - } - } - - rmdir($folderPath); - } - - /** - * All I/O operations must occur inside the base folder, for security reasons. - * This function will throw an exception if the folder where the I/O operation - * should occur is not inside the base folder. - * - * @param string $operationFolderPath The path of the folder where the I/O operation should occur - * @return void - * @throws \Box\Spout\Common\Exception\IOException If the folder where the I/O operation should occur is not inside the base folder - */ - protected function throwIfOperationNotInBaseFolder($operationFolderPath) - { - $operationFolderRealPath = realpath($operationFolderPath); - $isInBaseFolder = (strpos($operationFolderRealPath, $this->baseFolderRealPath) === 0); - if (!$isInBaseFolder) { - throw new IOException("Cannot perform I/O operation outside of the base folder: {$this->baseFolderRealPath}"); - } - } -} diff --git a/src/Spout/Common/Helper/GlobalFunctionsHelper.php b/src/Spout/Common/Helper/GlobalFunctionsHelper.php deleted file mode 100644 index c5d6e31..0000000 --- a/src/Spout/Common/Helper/GlobalFunctionsHelper.php +++ /dev/null @@ -1,318 +0,0 @@ -convertToUseRealPath($filePath); - return file_get_contents($realFilePath); - } - - /** - * Updates the given file path to use a real path. - * This is to avoid issues on some Windows setup. - * - * @param string $filePath File path - * @return string The file path using a real path - */ - protected function convertToUseRealPath($filePath) - { - $realFilePath = $filePath; - - if ($this->isZipStream($filePath)) { - if (preg_match('/zip:\/\/(.*)#(.*)/', $filePath, $matches)) { - $documentPath = $matches[1]; - $documentInsideZipPath = $matches[2]; - $realFilePath = 'zip://' . realpath($documentPath) . '#' . $documentInsideZipPath; - } - } else { - $realFilePath = realpath($filePath); - } - - return $realFilePath; - } - - /** - * Returns whether the given path is a zip stream. - * - * @param string $path Path pointing to a document - * @return bool TRUE if path is a zip stream, FALSE otherwise - */ - protected function isZipStream($path) - { - return (strpos($path, 'zip://') === 0); - } - - /** - * Wrapper around global function feof() - * @see feof() - * - * @param resource - * @return bool - */ - public function feof($handle) - { - return feof($handle); - } - - /** - * Wrapper around global function is_readable() - * @see is_readable() - * - * @param string $fileName - * @return bool - */ - public function is_readable($fileName) - { - return is_readable($fileName); - } - - /** - * Wrapper around global function basename() - * @see basename() - * - * @param string $path - * @param string|void $suffix - * @return string - */ - public function basename($path, $suffix = null) - { - return basename($path, $suffix); - } - - /** - * Wrapper around global function header() - * @see header() - * - * @param string $string - * @return void - */ - public function header($string) - { - header($string); - } - - /** - * Wrapper around global function ob_end_clean() - * @see ob_end_clean() - * - * @return void - */ - public function ob_end_clean() - { - if (ob_get_length() > 0) { - ob_end_clean(); - } - } - - /** - * Wrapper around global function iconv() - * @see iconv() - * - * @param string $string The string to be converted - * @param string $sourceEncoding The encoding of the source string - * @param string $targetEncoding The encoding the source string should be converted to - * @return string|bool the converted string or FALSE on failure. - */ - public function iconv($string, $sourceEncoding, $targetEncoding) - { - return iconv($sourceEncoding, $targetEncoding, $string); - } - - /** - * Wrapper around global function mb_convert_encoding() - * @see mb_convert_encoding() - * - * @param string $string The string to be converted - * @param string $sourceEncoding The encoding of the source string - * @param string $targetEncoding The encoding the source string should be converted to - * @return string|bool the converted string or FALSE on failure. - */ - public function mb_convert_encoding($string, $sourceEncoding, $targetEncoding) - { - return mb_convert_encoding($string, $targetEncoding, $sourceEncoding); - } - - /** - * Wrapper around global function stream_get_wrappers() - * @see stream_get_wrappers() - * - * @return array - */ - public function stream_get_wrappers() - { - return stream_get_wrappers(); - } - - /** - * Wrapper around global function function_exists() - * @see function_exists() - * - * @param string $functionName - * @return bool - */ - public function function_exists($functionName) - { - return function_exists($functionName); - } -} diff --git a/src/Spout/Common/Helper/StringHelper.php b/src/Spout/Common/Helper/StringHelper.php deleted file mode 100644 index 273104e..0000000 --- a/src/Spout/Common/Helper/StringHelper.php +++ /dev/null @@ -1,71 +0,0 @@ -hasMbstringSupport = extension_loaded('mbstring'); - } - - /** - * Returns the length of the given string. - * It uses the multi-bytes function is available. - * @see strlen - * @see mb_strlen - * - * @param string $string - * @return int - */ - public function getStringLength($string) - { - return $this->hasMbstringSupport ? mb_strlen($string) : strlen($string); - } - - /** - * Returns the position of the first occurrence of the given character/substring within the given string. - * It uses the multi-bytes function is available. - * @see strpos - * @see mb_strpos - * - * @param string $char Needle - * @param string $string Haystack - * @return int Char/substring's first occurrence position within the string if found (starts at 0) or -1 if not found - */ - public function getCharFirstOccurrencePosition($char, $string) - { - $position = $this->hasMbstringSupport ? mb_strpos($string, $char) : strpos($string, $char); - return ($position !== false) ? $position : -1; - } - - /** - * Returns the position of the last occurrence of the given character/substring within the given string. - * It uses the multi-bytes function is available. - * @see strrpos - * @see mb_strrpos - * - * @param string $char Needle - * @param string $string Haystack - * @return int Char/substring's last occurrence position within the string if found (starts at 0) or -1 if not found - */ - public function getCharLastOccurrencePosition($char, $string) - { - $position = $this->hasMbstringSupport ? mb_strrpos($string, $char) : strrpos($string, $char); - return ($position !== false) ? $position : -1; - } -} diff --git a/src/Spout/Common/Singleton.php b/src/Spout/Common/Singleton.php deleted file mode 100644 index 015ede8..0000000 --- a/src/Spout/Common/Singleton.php +++ /dev/null @@ -1,41 +0,0 @@ -init(); - } - - /** - * Initializes the singleton - * @return void - */ - protected function init() {} - - final private function __wakeup() {} - final private function __clone() {} -} diff --git a/src/Spout/Common/Type.php b/src/Spout/Common/Type.php deleted file mode 100644 index 5e9b75e..0000000 --- a/src/Spout/Common/Type.php +++ /dev/null @@ -1,16 +0,0 @@ -globalFunctionsHelper = $globalFunctionsHelper; - return $this; - } - - /** - * Sets whether date/time values should be returned as PHP objects or be formatted as strings. - * - * @api - * @param bool $shouldFormatDates - * @return AbstractReader - */ - public function setShouldFormatDates($shouldFormatDates) - { - $this->getOptions()->setShouldFormatDates($shouldFormatDates); - return $this; - } - - /** - * Sets whether empty rows should be returned or skipped. - * - * @api - * @param bool $shouldPreserveEmptyRows - * @return AbstractReader - */ - public function setShouldPreserveEmptyRows($shouldPreserveEmptyRows) - { - $this->getOptions()->setShouldPreserveEmptyRows($shouldPreserveEmptyRows); - return $this; - } - - /** - * Prepares the reader to read the given file. It also makes sure - * that the file exists and is readable. - * - * @api - * @param string $filePath Path of the file to be read - * @return void - * @throws \Box\Spout\Common\Exception\IOException If the file at the given path does not exist, is not readable or is corrupted - */ - public function open($filePath) - { - if ($this->isStreamWrapper($filePath) && (!$this->doesSupportStreamWrapper() || !$this->isSupportedStreamWrapper($filePath))) { - throw new IOException("Could not open $filePath for reading! Stream wrapper used is not supported for this type of file."); - } - - if (!$this->isPhpStream($filePath)) { - // we skip the checks if the provided file path points to a PHP stream - if (!$this->globalFunctionsHelper->file_exists($filePath)) { - throw new IOException("Could not open $filePath for reading! File does not exist."); - } else if (!$this->globalFunctionsHelper->is_readable($filePath)) { - throw new IOException("Could not open $filePath for reading! File is not readable."); - } - } - - try { - $fileRealPath = $this->getFileRealPath($filePath); - $this->openReader($fileRealPath); - $this->isStreamOpened = true; - } catch (\Exception $exception) { - throw new IOException("Could not open $filePath for reading! ({$exception->getMessage()})"); - } - } - - /** - * Returns the real path of the given path. - * If the given path is a valid stream wrapper, returns the path unchanged. - * - * @param string $filePath - * @return string - */ - protected function getFileRealPath($filePath) - { - if ($this->isSupportedStreamWrapper($filePath)) { - return $filePath; - } - - // Need to use realpath to fix "Can't open file" on some Windows setup - return realpath($filePath); - } - - /** - * Returns the scheme of the custom stream wrapper, if the path indicates a stream wrapper is used. - * For example, php://temp => php, s3://path/to/file => s3... - * - * @param string $filePath Path of the file to be read - * @return string|null The stream wrapper scheme or NULL if not a stream wrapper - */ - protected function getStreamWrapperScheme($filePath) - { - $streamScheme = null; - if (preg_match('/^(\w+):\/\//', $filePath, $matches)) { - $streamScheme = $matches[1]; - } - return $streamScheme; - } - - /** - * Checks if the given path is an unsupported stream wrapper - * (like local path, php://temp, mystream://foo/bar...). - * - * @param string $filePath Path of the file to be read - * @return bool Whether the given path is an unsupported stream wrapper - */ - protected function isStreamWrapper($filePath) - { - return ($this->getStreamWrapperScheme($filePath) !== null); - } - - /** - * Checks if the given path is an supported stream wrapper - * (like php://temp, mystream://foo/bar...). - * If the given path is a local path, returns true. - * - * @param string $filePath Path of the file to be read - * @return bool Whether the given path is an supported stream wrapper - */ - protected function isSupportedStreamWrapper($filePath) - { - $streamScheme = $this->getStreamWrapperScheme($filePath); - return ($streamScheme !== null) ? - in_array($streamScheme, $this->globalFunctionsHelper->stream_get_wrappers()) : - true; - } - - /** - * Checks if a path is a PHP stream (like php://output, php://memory, ...) - * - * @param string $filePath Path of the file to be read - * @return bool Whether the given path maps to a PHP stream - */ - protected function isPhpStream($filePath) - { - $streamScheme = $this->getStreamWrapperScheme($filePath); - return ($streamScheme === 'php'); - } - - /** - * Returns an iterator to iterate over sheets. - * - * @api - * @return \Iterator To iterate over sheets - * @throws \Box\Spout\Reader\Exception\ReaderNotOpenedException If called before opening the reader - */ - public function getSheetIterator() - { - if (!$this->isStreamOpened) { - throw new ReaderNotOpenedException('Reader should be opened first.'); - } - - return $this->getConcreteSheetIterator(); - } - - /** - * Closes the reader, preventing any additional reading - * - * @api - * @return void - */ - public function close() - { - if ($this->isStreamOpened) { - $this->closeReader(); - - $sheetIterator = $this->getConcreteSheetIterator(); - if ($sheetIterator) { - $sheetIterator->end(); - } - - $this->isStreamOpened = false; - } - } -} diff --git a/src/Spout/Reader/CSV/Reader.php b/src/Spout/Reader/CSV/Reader.php deleted file mode 100644 index 648a12d..0000000 --- a/src/Spout/Reader/CSV/Reader.php +++ /dev/null @@ -1,149 +0,0 @@ -options)) { - $this->options = new ReaderOptions(); - } - return $this->options; - } - - /** - * Sets the field delimiter for the CSV. - * Needs to be called before opening the reader. - * - * @param string $fieldDelimiter Character that delimits fields - * @return Reader - */ - public function setFieldDelimiter($fieldDelimiter) - { - $this->getOptions()->setFieldDelimiter($fieldDelimiter); - return $this; - } - - /** - * Sets the field enclosure for the CSV. - * Needs to be called before opening the reader. - * - * @param string $fieldEnclosure Character that enclose fields - * @return Reader - */ - public function setFieldEnclosure($fieldEnclosure) - { - $this->getOptions()->setFieldEnclosure($fieldEnclosure); - return $this; - } - - /** - * Sets the encoding of the CSV file to be read. - * Needs to be called before opening the reader. - * - * @param string $encoding Encoding of the CSV file to be read - * @return Reader - */ - public function setEncoding($encoding) - { - $this->getOptions()->setEncoding($encoding); - return $this; - } - - /** - * Sets the EOL for the CSV. - * Needs to be called before opening the reader. - * - * @param string $endOfLineCharacter used to properly get lines from the CSV file. - * @return Reader - */ - public function setEndOfLineCharacter($endOfLineCharacter) - { - $this->getOptions()->setEndOfLineCharacter($endOfLineCharacter); - return $this; - } - - /** - * Returns whether stream wrappers are supported - * - * @return bool - */ - protected function doesSupportStreamWrapper() - { - return true; - } - - /** - * Opens the file at the given path to make it ready to be read. - * If setEncoding() was not called, it assumes that the file is encoded in UTF-8. - * - * @param string $filePath Path of the CSV file to be read - * @return void - * @throws \Box\Spout\Common\Exception\IOException - */ - protected function openReader($filePath) - { - $this->originalAutoDetectLineEndings = ini_get('auto_detect_line_endings'); - ini_set('auto_detect_line_endings', '1'); - - $this->filePointer = $this->globalFunctionsHelper->fopen($filePath, 'r'); - if (!$this->filePointer) { - throw new IOException("Could not open file $filePath for reading."); - } - - $this->sheetIterator = new SheetIterator( - $this->filePointer, - $this->getOptions(), - $this->globalFunctionsHelper - ); - } - - /** - * Returns an iterator to iterate over sheets. - * - * @return SheetIterator To iterate over sheets - */ - protected function getConcreteSheetIterator() - { - return $this->sheetIterator; - } - - - /** - * Closes the reader. To be used after reading the file. - * - * @return void - */ - protected function closeReader() - { - if ($this->filePointer) { - $this->globalFunctionsHelper->fclose($this->filePointer); - } - - ini_set('auto_detect_line_endings', $this->originalAutoDetectLineEndings); - } -} diff --git a/src/Spout/Reader/CSV/ReaderOptions.php b/src/Spout/Reader/CSV/ReaderOptions.php deleted file mode 100644 index 9a1adb8..0000000 --- a/src/Spout/Reader/CSV/ReaderOptions.php +++ /dev/null @@ -1,110 +0,0 @@ -fieldDelimiter; - } - - /** - * Sets the field delimiter for the CSV. - * Needs to be called before opening the reader. - * - * @param string $fieldDelimiter Character that delimits fields - * @return ReaderOptions - */ - public function setFieldDelimiter($fieldDelimiter) - { - $this->fieldDelimiter = $fieldDelimiter; - return $this; - } - - /** - * @return string - */ - public function getFieldEnclosure() - { - return $this->fieldEnclosure; - } - - /** - * Sets the field enclosure for the CSV. - * Needs to be called before opening the reader. - * - * @param string $fieldEnclosure Character that enclose fields - * @return ReaderOptions - */ - public function setFieldEnclosure($fieldEnclosure) - { - $this->fieldEnclosure = $fieldEnclosure; - return $this; - } - - /** - * @return string - */ - public function getEncoding() - { - return $this->encoding; - } - - /** - * Sets the encoding of the CSV file to be read. - * Needs to be called before opening the reader. - * - * @param string $encoding Encoding of the CSV file to be read - * @return ReaderOptions - */ - public function setEncoding($encoding) - { - $this->encoding = $encoding; - return $this; - } - - /** - * @return string EOL for the CSV - */ - public function getEndOfLineCharacter() - { - return $this->endOfLineCharacter; - } - - /** - * Sets the EOL for the CSV. - * Needs to be called before opening the reader. - * - * @param string $endOfLineCharacter used to properly get lines from the CSV file. - * @return ReaderOptions - */ - public function setEndOfLineCharacter($endOfLineCharacter) - { - $this->endOfLineCharacter = $endOfLineCharacter; - return $this; - } -} diff --git a/src/Spout/Reader/CSV/RowIterator.php b/src/Spout/Reader/CSV/RowIterator.php deleted file mode 100644 index a2a6672..0000000 --- a/src/Spout/Reader/CSV/RowIterator.php +++ /dev/null @@ -1,260 +0,0 @@ -filePointer = $filePointer; - $this->fieldDelimiter = $options->getFieldDelimiter(); - $this->fieldEnclosure = $options->getFieldEnclosure(); - $this->encoding = $options->getEncoding(); - $this->inputEOLDelimiter = $options->getEndOfLineCharacter(); - $this->shouldPreserveEmptyRows = $options->shouldPreserveEmptyRows(); - $this->globalFunctionsHelper = $globalFunctionsHelper; - - $this->encodingHelper = new EncodingHelper($globalFunctionsHelper); - } - - /** - * Rewind the Iterator to the first element - * @link http://php.net/manual/en/iterator.rewind.php - * - * @return void - */ - public function rewind() - { - $this->rewindAndSkipBom(); - - $this->numReadRows = 0; - $this->rowDataBuffer = null; - - $this->next(); - } - - /** - * This rewinds and skips the BOM if inserted at the beginning of the file - * by moving the file pointer after it, so that it is not read. - * - * @return void - */ - protected function rewindAndSkipBom() - { - $byteOffsetToSkipBom = $this->encodingHelper->getBytesOffsetToSkipBOM($this->filePointer, $this->encoding); - - // sets the cursor after the BOM (0 means no BOM, so rewind it) - $this->globalFunctionsHelper->fseek($this->filePointer, $byteOffsetToSkipBom); - } - - /** - * Checks if current position is valid - * @link http://php.net/manual/en/iterator.valid.php - * - * @return bool - */ - public function valid() - { - return ($this->filePointer && !$this->hasReachedEndOfFile); - } - - /** - * Move forward to next element. Reads data for the next unprocessed row. - * @link http://php.net/manual/en/iterator.next.php - * - * @return void - * @throws \Box\Spout\Common\Exception\EncodingConversionException If unable to convert data to UTF-8 - */ - public function next() - { - $this->hasReachedEndOfFile = $this->globalFunctionsHelper->feof($this->filePointer); - - if (!$this->hasReachedEndOfFile) { - $this->readDataForNextRow(); - } - } - - /** - * @return void - * @throws \Box\Spout\Common\Exception\EncodingConversionException If unable to convert data to UTF-8 - */ - protected function readDataForNextRow() - { - do { - $rowData = $this->getNextUTF8EncodedRow(); - } while ($this->shouldReadNextRow($rowData)); - - if ($rowData !== false) { - // str_replace will replace NULL values by empty strings - $this->rowDataBuffer = str_replace(null, null, $rowData); - $this->numReadRows++; - } else { - // If we reach this point, it means end of file was reached. - // This happens when the last lines are empty lines. - $this->hasReachedEndOfFile = true; - } - } - - /** - * @param array|bool $currentRowData - * @return bool Whether the data for the current row can be returned or if we need to keep reading - */ - protected function shouldReadNextRow($currentRowData) - { - $hasSuccessfullyFetchedRowData = ($currentRowData !== false); - $hasNowReachedEndOfFile = $this->globalFunctionsHelper->feof($this->filePointer); - $isEmptyLine = $this->isEmptyLine($currentRowData); - - return ( - (!$hasSuccessfullyFetchedRowData && !$hasNowReachedEndOfFile) || - (!$this->shouldPreserveEmptyRows && $isEmptyLine) - ); - } - - /** - * Returns the next row, converted if necessary to UTF-8. - * As fgetcsv() does not manage correctly encoding for non UTF-8 data, - * we remove manually whitespace with ltrim or rtrim (depending on the order of the bytes) - * - * @return array|false The row for the current file pointer, encoded in UTF-8 or FALSE if nothing to read - * @throws \Box\Spout\Common\Exception\EncodingConversionException If unable to convert data to UTF-8 - */ - protected function getNextUTF8EncodedRow() - { - $encodedRowData = $this->globalFunctionsHelper->fgetcsv($this->filePointer, self::MAX_READ_BYTES_PER_LINE, $this->fieldDelimiter, $this->fieldEnclosure); - if ($encodedRowData === false) { - return false; - } - - foreach ($encodedRowData as $cellIndex => $cellValue) { - switch($this->encoding) { - case EncodingHelper::ENCODING_UTF16_LE: - case EncodingHelper::ENCODING_UTF32_LE: - // remove whitespace from the beginning of a string as fgetcsv() add extra whitespace when it try to explode non UTF-8 data - $cellValue = ltrim($cellValue); - break; - - case EncodingHelper::ENCODING_UTF16_BE: - case EncodingHelper::ENCODING_UTF32_BE: - // remove whitespace from the end of a string as fgetcsv() add extra whitespace when it try to explode non UTF-8 data - $cellValue = rtrim($cellValue); - break; - } - - $encodedRowData[$cellIndex] = $this->encodingHelper->attemptConversionToUTF8($cellValue, $this->encoding); - } - - return $encodedRowData; - } - - /** - * Returns the end of line delimiter, encoded using the same encoding as the CSV. - * The return value is cached. - * - * @return string - */ - protected function getEncodedEOLDelimiter() - { - if (!isset($this->encodedEOLDelimiter)) { - $this->encodedEOLDelimiter = $this->encodingHelper->attemptConversionFromUTF8($this->inputEOLDelimiter, $this->encoding); - } - - return $this->encodedEOLDelimiter; - } - - /** - * @param array|bool $lineData Array containing the cells value for the line - * @return bool Whether the given line is empty - */ - protected function isEmptyLine($lineData) - { - return (is_array($lineData) && count($lineData) === 1 && $lineData[0] === null); - } - - /** - * Return the current element from the buffer - * @link http://php.net/manual/en/iterator.current.php - * - * @return array|null - */ - public function current() - { - return $this->rowDataBuffer; - } - - /** - * Return the key of the current element - * @link http://php.net/manual/en/iterator.key.php - * - * @return int - */ - public function key() - { - return $this->numReadRows; - } - - /** - * Cleans up what was created to iterate over the object. - * - * @return void - */ - public function end() - { - // do nothing - } -} diff --git a/src/Spout/Reader/CSV/Sheet.php b/src/Spout/Reader/CSV/Sheet.php deleted file mode 100644 index 9a688db..0000000 --- a/src/Spout/Reader/CSV/Sheet.php +++ /dev/null @@ -1,62 +0,0 @@ -rowIterator = new RowIterator($filePointer, $options, $globalFunctionsHelper); - } - - /** - * @api - * @return \Box\Spout\Reader\CSV\RowIterator - */ - public function getRowIterator() - { - return $this->rowIterator; - } - - /** - * @api - * @return int Index of the sheet - */ - public function getIndex() - { - return 0; - } - - /** - * @api - * @return string Name of the sheet - empty string since CSV does not support that - */ - public function getName() - { - return ''; - } - - /** - * @api - * @return bool Always TRUE as there is only one sheet - */ - public function isActive() - { - return true; - } -} diff --git a/src/Spout/Reader/CSV/SheetIterator.php b/src/Spout/Reader/CSV/SheetIterator.php deleted file mode 100644 index 58a9480..0000000 --- a/src/Spout/Reader/CSV/SheetIterator.php +++ /dev/null @@ -1,95 +0,0 @@ -sheet = new Sheet($filePointer, $options, $globalFunctionsHelper); - } - - /** - * Rewind the Iterator to the first element - * @link http://php.net/manual/en/iterator.rewind.php - * - * @return void - */ - public function rewind() - { - $this->hasReadUniqueSheet = false; - } - - /** - * Checks if current position is valid - * @link http://php.net/manual/en/iterator.valid.php - * - * @return bool - */ - public function valid() - { - return (!$this->hasReadUniqueSheet); - } - - /** - * Move forward to next element - * @link http://php.net/manual/en/iterator.next.php - * - * @return void - */ - public function next() - { - $this->hasReadUniqueSheet = true; - } - - /** - * Return the current element - * @link http://php.net/manual/en/iterator.current.php - * - * @return \Box\Spout\Reader\CSV\Sheet - */ - public function current() - { - return $this->sheet; - } - - /** - * Return the key of the current element - * @link http://php.net/manual/en/iterator.key.php - * - * @return int - */ - public function key() - { - return 1; - } - - /** - * Cleans up what was created to iterate over the object. - * - * @return void - */ - public function end() - { - // do nothing - } -} diff --git a/src/Spout/Reader/Common/ReaderOptions.php b/src/Spout/Reader/Common/ReaderOptions.php deleted file mode 100644 index 4ab7a07..0000000 --- a/src/Spout/Reader/Common/ReaderOptions.php +++ /dev/null @@ -1,58 +0,0 @@ -shouldFormatDates; - } - - /** - * Sets whether date/time values should be returned as PHP objects or be formatted as strings. - * - * @param bool $shouldFormatDates - * @return ReaderOptions - */ - public function setShouldFormatDates($shouldFormatDates) - { - $this->shouldFormatDates = $shouldFormatDates; - return $this; - } - - /** - * @return bool Whether empty rows should be returned or skipped. - */ - public function shouldPreserveEmptyRows() - { - return $this->shouldPreserveEmptyRows; - } - - /** - * Sets whether empty rows should be returned or skipped. - * - * @param bool $shouldPreserveEmptyRows - * @return ReaderOptions - */ - public function setShouldPreserveEmptyRows($shouldPreserveEmptyRows) - { - $this->shouldPreserveEmptyRows = $shouldPreserveEmptyRows; - return $this; - } -} diff --git a/src/Spout/Reader/Common/XMLProcessor.php b/src/Spout/Reader/Common/XMLProcessor.php deleted file mode 100644 index d8a1da8..0000000 --- a/src/Spout/Reader/Common/XMLProcessor.php +++ /dev/null @@ -1,152 +0,0 @@ -xmlReader = $xmlReader; - } - - /** - * @param string $nodeName A callback may be triggered when a node with this name is read - * @param int $nodeType Type of the node [NODE_TYPE_START || NODE_TYPE_END] - * @param callable $callback Callback to execute when the read node has the given name and type - * @return XMLProcessor - */ - public function registerCallback($nodeName, $nodeType, $callback) - { - $callbackKey = $this->getCallbackKey($nodeName, $nodeType); - $this->callbacks[$callbackKey] = $this->getInvokableCallbackData($callback); - - return $this; - } - - /** - * @param string $nodeName Name of the node - * @param int $nodeType Type of the node [NODE_TYPE_START || NODE_TYPE_END] - * @return string Key used to store the associated callback - */ - private function getCallbackKey($nodeName, $nodeType) - { - return "$nodeName$nodeType"; - } - - /** - * Because the callback can be a "protected" function, we don't want to use call_user_func() directly - * but instead invoke the callback using Reflection. This allows the invocation of "protected" functions. - * Since some functions can be called a lot, we pre-process the callback to only return the elements that - * will be needed to invoke the callback later. - * - * @param callable $callback Array reference to a callback: [OBJECT, METHOD_NAME] - * @return array Associative array containing the elements needed to invoke the callback using Reflection - */ - private function getInvokableCallbackData($callback) - { - $callbackObject = $callback[0]; - $callbackMethodName = $callback[1]; - $reflectionMethod = new \ReflectionMethod(get_class($callbackObject), $callbackMethodName); - $reflectionMethod->setAccessible(true); - - return [ - self::CALLBACK_REFLECTION_METHOD => $reflectionMethod, - self::CALLBACK_REFLECTION_OBJECT => $callbackObject, - ]; - } - - /** - * Resumes the reading of the XML file where it was left off. - * Stops whenever a callback indicates that reading should stop or at the end of the file. - * - * @return void - * @throws \Box\Spout\Reader\Exception\XMLProcessingException - */ - public function readUntilStopped() - { - while ($this->xmlReader->read()) { - $nodeType = $this->xmlReader->nodeType; - $nodeNamePossiblyWithPrefix = $this->xmlReader->name; - $nodeNameWithoutPrefix = $this->xmlReader->localName; - - $callbackData = $this->getRegisteredCallbackData($nodeNamePossiblyWithPrefix, $nodeNameWithoutPrefix, $nodeType); - - if ($callbackData !== null) { - $callbackResponse = $this->invokeCallback($callbackData, [$this->xmlReader]); - - if ($callbackResponse === self::PROCESSING_STOP) { - // stop reading - break; - } - } - } - } - - /** - * @param string $nodeNamePossiblyWithPrefix Name of the node, possibly prefixed - * @param string $nodeNameWithoutPrefix Name of the same node, un-prefixed - * @param int $nodeType Type of the node [NODE_TYPE_START || NODE_TYPE_END] - * @return array|null Callback data to be used for execution when a node of the given name/type is read or NULL if none found - */ - private function getRegisteredCallbackData($nodeNamePossiblyWithPrefix, $nodeNameWithoutPrefix, $nodeType) - { - // With prefixed nodes, we should match if (by order of preference): - // 1. the callback was registered with the prefixed node name (e.g. "x:worksheet") - // 2. the callback was registered with the un-prefixed node name (e.g. "worksheet") - $callbackKeyForPossiblyPrefixedName = $this->getCallbackKey($nodeNamePossiblyWithPrefix, $nodeType); - $callbackKeyForUnPrefixedName = $this->getCallbackKey($nodeNameWithoutPrefix, $nodeType); - $hasPrefix = ($nodeNamePossiblyWithPrefix !== $nodeNameWithoutPrefix); - - $callbackKeyToUse = $callbackKeyForUnPrefixedName; - if ($hasPrefix && isset($this->callbacks[$callbackKeyForPossiblyPrefixedName])) { - $callbackKeyToUse = $callbackKeyForPossiblyPrefixedName; - } - - // Using isset here because it is way faster than array_key_exists... - return isset($this->callbacks[$callbackKeyToUse]) ? $this->callbacks[$callbackKeyToUse] : null; - } - - /** - * @param array $callbackData Associative array containing data to invoke the callback using Reflection - * @param array $args Arguments to pass to the callback - * @return int Callback response - */ - private function invokeCallback($callbackData, $args) - { - $reflectionMethod = $callbackData[self::CALLBACK_REFLECTION_METHOD]; - $callbackObject = $callbackData[self::CALLBACK_REFLECTION_OBJECT]; - - return $reflectionMethod->invokeArgs($callbackObject, $args); - } -} diff --git a/src/Spout/Reader/Exception/IteratorNotRewindableException.php b/src/Spout/Reader/Exception/IteratorNotRewindableException.php deleted file mode 100644 index a030c12..0000000 --- a/src/Spout/Reader/Exception/IteratorNotRewindableException.php +++ /dev/null @@ -1,13 +0,0 @@ -shouldFormatDates = $shouldFormatDates; - - /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ - $this->escaper = \Box\Spout\Common\Escaper\ODS::getInstance(); - } - - /** - * Returns the (unescaped) correctly marshalled, cell value associated to the given XML node. - * @see http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#refTable13 - * - * @param \DOMNode $node - * @return string|int|float|bool|\DateTime|\DateInterval|null The value associated with the cell, empty string if cell's type is void/undefined, null on error - */ - public function extractAndFormatNodeValue($node) - { - $cellType = $node->getAttribute(self::XML_ATTRIBUTE_TYPE); - - switch ($cellType) { - case self::CELL_TYPE_STRING: - return $this->formatStringCellValue($node); - case self::CELL_TYPE_FLOAT: - return $this->formatFloatCellValue($node); - case self::CELL_TYPE_BOOLEAN: - return $this->formatBooleanCellValue($node); - case self::CELL_TYPE_DATE: - return $this->formatDateCellValue($node); - case self::CELL_TYPE_TIME: - return $this->formatTimeCellValue($node); - case self::CELL_TYPE_CURRENCY: - return $this->formatCurrencyCellValue($node); - case self::CELL_TYPE_PERCENTAGE: - return $this->formatPercentageCellValue($node); - case self::CELL_TYPE_VOID: - default: - return ''; - } - } - - /** - * Returns the cell String value. - * - * @param \DOMNode $node - * @return string The value associated with the cell - */ - protected function formatStringCellValue($node) - { - $pNodeValues = []; - $pNodes = $node->getElementsByTagName(self::XML_NODE_P); - - foreach ($pNodes as $pNode) { - $currentPValue = ''; - - foreach ($pNode->childNodes as $childNode) { - if ($childNode instanceof \DOMText) { - $currentPValue .= $childNode->nodeValue; - } else if ($childNode->nodeName === self::XML_NODE_S) { - $spaceAttribute = $childNode->getAttribute(self::XML_ATTRIBUTE_C); - $numSpaces = (!empty($spaceAttribute)) ? intval($spaceAttribute) : 1; - $currentPValue .= str_repeat(' ', $numSpaces); - } else if ($childNode->nodeName === self::XML_NODE_A || $childNode->nodeName === self::XML_NODE_SPAN) { - $currentPValue .= $childNode->nodeValue; - } - } - - $pNodeValues[] = $currentPValue; - } - - $escapedCellValue = implode("\n", $pNodeValues); - $cellValue = $this->escaper->unescape($escapedCellValue); - return $cellValue; - } - - /** - * Returns the cell Numeric value from the given node. - * - * @param \DOMNode $node - * @return int|float The value associated with the cell - */ - protected function formatFloatCellValue($node) - { - $nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_VALUE); - $nodeIntValue = intval($nodeValue); - // The "==" is intentionally not a "===" because only the value matters, not the type - $cellValue = ($nodeIntValue == $nodeValue) ? $nodeIntValue : floatval($nodeValue); - return $cellValue; - } - - /** - * Returns the cell Boolean value from the given node. - * - * @param \DOMNode $node - * @return bool The value associated with the cell - */ - protected function formatBooleanCellValue($node) - { - $nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_BOOLEAN_VALUE); - // !! is similar to boolval() - $cellValue = !!$nodeValue; - return $cellValue; - } - - /** - * Returns the cell Date value from the given node. - * - * @param \DOMNode $node - * @return \DateTime|string|null The value associated with the cell or NULL if invalid date value - */ - protected function formatDateCellValue($node) - { - // The XML node looks like this: - // - // 05/19/16 04:39 PM - // - - if ($this->shouldFormatDates) { - // The date is already formatted in the "p" tag - $nodeWithValueAlreadyFormatted = $node->getElementsByTagName(self::XML_NODE_P)->item(0); - return $nodeWithValueAlreadyFormatted->nodeValue; - } else { - // otherwise, get it from the "date-value" attribute - try { - $nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_DATE_VALUE); - return new \DateTime($nodeValue); - } catch (\Exception $e) { - return null; - } - } - } - - /** - * Returns the cell Time value from the given node. - * - * @param \DOMNode $node - * @return \DateInterval|string|null The value associated with the cell or NULL if invalid time value - */ - protected function formatTimeCellValue($node) - { - // The XML node looks like this: - // - // 01:24:00 PM - // - - if ($this->shouldFormatDates) { - // The date is already formatted in the "p" tag - $nodeWithValueAlreadyFormatted = $node->getElementsByTagName(self::XML_NODE_P)->item(0); - return $nodeWithValueAlreadyFormatted->nodeValue; - } else { - // otherwise, get it from the "time-value" attribute - try { - $nodeValue = $node->getAttribute(self::XML_ATTRIBUTE_TIME_VALUE); - return new \DateInterval($nodeValue); - } catch (\Exception $e) { - return null; - } - } - } - - /** - * Returns the cell Currency value from the given node. - * - * @param \DOMNode $node - * @return string The value associated with the cell (e.g. "100 USD" or "9.99 EUR") - */ - protected function formatCurrencyCellValue($node) - { - $value = $node->getAttribute(self::XML_ATTRIBUTE_VALUE); - $currency = $node->getAttribute(self::XML_ATTRIBUTE_CURRENCY); - - return "$value $currency"; - } - - /** - * Returns the cell Percentage value from the given node. - * - * @param \DOMNode $node - * @return int|float The value associated with the cell - */ - protected function formatPercentageCellValue($node) - { - // percentages are formatted like floats - return $this->formatFloatCellValue($node); - } -} diff --git a/src/Spout/Reader/ODS/Helper/SettingsHelper.php b/src/Spout/Reader/ODS/Helper/SettingsHelper.php deleted file mode 100644 index a5388ef..0000000 --- a/src/Spout/Reader/ODS/Helper/SettingsHelper.php +++ /dev/null @@ -1,51 +0,0 @@ -openFileInZip($filePath, self::SETTINGS_XML_FILE_PATH) === false) { - return null; - } - - $activeSheetName = null; - - try { - while ($xmlReader->readUntilNodeFound(self::XML_NODE_CONFIG_ITEM)) { - if ($xmlReader->getAttribute(self::XML_ATTRIBUTE_CONFIG_NAME) === self::XML_ATTRIBUTE_VALUE_ACTIVE_TABLE) { - $activeSheetName = $xmlReader->readString(); - break; - } - } - } catch (XMLProcessingException $exception) { - // do nothing - } - - $xmlReader->close(); - - return $activeSheetName; - } -} diff --git a/src/Spout/Reader/ODS/Reader.php b/src/Spout/Reader/ODS/Reader.php deleted file mode 100644 index dbdc47b..0000000 --- a/src/Spout/Reader/ODS/Reader.php +++ /dev/null @@ -1,85 +0,0 @@ -options)) { - $this->options = new ReaderOptions(); - } - return $this->options; - } - - /** - * Returns whether stream wrappers are supported - * - * @return bool - */ - protected function doesSupportStreamWrapper() - { - return false; - } - - /** - * Opens the file at the given file path to make it ready to be read. - * - * @param string $filePath Path of the file to be read - * @return void - * @throws \Box\Spout\Common\Exception\IOException If the file at the given path or its content cannot be read - * @throws \Box\Spout\Reader\Exception\NoSheetsFoundException If there are no sheets in the file - */ - protected function openReader($filePath) - { - $this->zip = new \ZipArchive(); - - if ($this->zip->open($filePath) === true) { - $this->sheetIterator = new SheetIterator($filePath, $this->getOptions()); - } else { - throw new IOException("Could not open $filePath for reading."); - } - } - - /** - * Returns an iterator to iterate over sheets. - * - * @return SheetIterator To iterate over sheets - */ - protected function getConcreteSheetIterator() - { - return $this->sheetIterator; - } - - /** - * Closes the reader. To be used after reading the file. - * - * @return void - */ - protected function closeReader() - { - if ($this->zip) { - $this->zip->close(); - } - } -} diff --git a/src/Spout/Reader/ODS/ReaderOptions.php b/src/Spout/Reader/ODS/ReaderOptions.php deleted file mode 100644 index 2d29640..0000000 --- a/src/Spout/Reader/ODS/ReaderOptions.php +++ /dev/null @@ -1,14 +0,0 @@ -" element - * @param \Box\Spout\Reader\ODS\ReaderOptions $options Reader's current options - */ - public function __construct($xmlReader, $options) - { - $this->xmlReader = $xmlReader; - $this->shouldPreserveEmptyRows = $options->shouldPreserveEmptyRows(); - $this->cellValueFormatter = new CellValueFormatter($options->shouldFormatDates()); - - // Register all callbacks to process different nodes when reading the XML file - $this->xmlProcessor = new XMLProcessor($this->xmlReader); - $this->xmlProcessor->registerCallback(self::XML_NODE_ROW, XMLProcessor::NODE_TYPE_START, [$this, 'processRowStartingNode']); - $this->xmlProcessor->registerCallback(self::XML_NODE_CELL, XMLProcessor::NODE_TYPE_START, [$this, 'processCellStartingNode']); - $this->xmlProcessor->registerCallback(self::XML_NODE_ROW, XMLProcessor::NODE_TYPE_END, [$this, 'processRowEndingNode']); - $this->xmlProcessor->registerCallback(self::XML_NODE_TABLE, XMLProcessor::NODE_TYPE_END, [$this, 'processTableEndingNode']); - } - - /** - * Rewind the Iterator to the first element. - * NOTE: It can only be done once, as it is not possible to read an XML file backwards. - * @link http://php.net/manual/en/iterator.rewind.php - * - * @return void - * @throws \Box\Spout\Reader\Exception\IteratorNotRewindableException If the iterator is rewound more than once - */ - public function rewind() - { - // Because sheet and row data is located in the file, we can't rewind both the - // sheet iterator and the row iterator, as XML file cannot be read backwards. - // Therefore, rewinding the row iterator has been disabled. - if ($this->hasAlreadyBeenRewound) { - throw new IteratorNotRewindableException(); - } - - $this->hasAlreadyBeenRewound = true; - $this->lastRowIndexProcessed = 0; - $this->nextRowIndexToBeProcessed = 1; - $this->rowDataBuffer = null; - $this->hasReachedEndOfFile = false; - - $this->next(); - } - - /** - * Checks if current position is valid - * @link http://php.net/manual/en/iterator.valid.php - * - * @return bool - */ - public function valid() - { - return (!$this->hasReachedEndOfFile); - } - - /** - * Move forward to next element. Empty rows will be skipped. - * @link http://php.net/manual/en/iterator.next.php - * - * @return void - * @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If a shared string was not found - * @throws \Box\Spout\Common\Exception\IOException If unable to read the sheet data XML - */ - public function next() - { - if ($this->doesNeedDataForNextRowToBeProcessed()) { - $this->readDataForNextRow(); - } - - $this->lastRowIndexProcessed++; - } - - /** - * Returns whether we need data for the next row to be processed. - * We DO need to read data if: - * - we have not read any rows yet - * OR - * - the next row to be processed immediately follows the last read row - * - * @return bool Whether we need data for the next row to be processed. - */ - protected function doesNeedDataForNextRowToBeProcessed() - { - $hasReadAtLeastOneRow = ($this->lastRowIndexProcessed !== 0); - - return ( - !$hasReadAtLeastOneRow || - $this->lastRowIndexProcessed === $this->nextRowIndexToBeProcessed - 1 - ); - } - - /** - * @return void - * @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If a shared string was not found - * @throws \Box\Spout\Common\Exception\IOException If unable to read the sheet data XML - */ - protected function readDataForNextRow() - { - $this->currentlyProcessedRowData = []; - - try { - $this->xmlProcessor->readUntilStopped(); - } catch (XMLProcessingException $exception) { - throw new IOException("The sheet's data cannot be read. [{$exception->getMessage()}]"); - } - - $this->rowDataBuffer = $this->currentlyProcessedRowData; - } - - /** - * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "" starting node - * @return int A return code that indicates what action should the processor take next - */ - protected function processRowStartingNode($xmlReader) - { - // Reset data from current row - $this->hasAlreadyReadOneCellInCurrentRow = false; - $this->lastProcessedCellValue = null; - $this->numColumnsRepeated = 1; - $this->numRowsRepeated = $this->getNumRowsRepeatedForCurrentNode($xmlReader); - - return XMLProcessor::PROCESSING_CONTINUE; - } - - /** - * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "" starting node - * @return int A return code that indicates what action should the processor take next - */ - protected function processCellStartingNode($xmlReader) - { - $currentNumColumnsRepeated = $this->getNumColumnsRepeatedForCurrentNode($xmlReader); - - // NOTE: expand() will automatically decode all XML entities of the child nodes - $node = $xmlReader->expand(); - $currentCellValue = $this->getCellValue($node); - - // process cell N only after having read cell N+1 (see below why) - if ($this->hasAlreadyReadOneCellInCurrentRow) { - for ($i = 0; $i < $this->numColumnsRepeated; $i++) { - $this->currentlyProcessedRowData[] = $this->lastProcessedCellValue; - } - } - - $this->hasAlreadyReadOneCellInCurrentRow = true; - $this->lastProcessedCellValue = $currentCellValue; - $this->numColumnsRepeated = $currentNumColumnsRepeated; - - return XMLProcessor::PROCESSING_CONTINUE; - } - - /** - * @return int A return code that indicates what action should the processor take next - */ - protected function processRowEndingNode() - { - $isEmptyRow = $this->isEmptyRow($this->currentlyProcessedRowData, $this->lastProcessedCellValue); - - // if the fetched row is empty and we don't want to preserve it... - if (!$this->shouldPreserveEmptyRows && $isEmptyRow) { - // ... skip it - return XMLProcessor::PROCESSING_CONTINUE; - } - - // if the row is empty, we don't want to return more than one cell - $actualNumColumnsRepeated = (!$isEmptyRow) ? $this->numColumnsRepeated : 1; - - // Only add the value if the last read cell is not a trailing empty cell repeater in Excel. - // The current count of read columns is determined by counting the values in "$this->currentlyProcessedRowData". - // This is to avoid creating a lot of empty cells, as Excel adds a last empty "" - // with a number-columns-repeated value equals to the number of (supported columns - used columns). - // In Excel, the number of supported columns is 16384, but we don't want to returns rows with - // always 16384 cells. - if ((count($this->currentlyProcessedRowData) + $actualNumColumnsRepeated) !== self::MAX_COLUMNS_EXCEL) { - for ($i = 0; $i < $actualNumColumnsRepeated; $i++) { - $this->currentlyProcessedRowData[] = $this->lastProcessedCellValue; - } - } - - // If we are processing row N and the row is repeated M times, - // then the next row to be processed will be row (N+M). - $this->nextRowIndexToBeProcessed += $this->numRowsRepeated; - - // at this point, we have all the data we need for the row - // so that we can populate the buffer - return XMLProcessor::PROCESSING_STOP; - } - - /** - * @return int A return code that indicates what action should the processor take next - */ - protected function processTableEndingNode() - { - // The closing "" marks the end of the file - $this->hasReachedEndOfFile = true; - - return XMLProcessor::PROCESSING_STOP; - } - - /** - * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "" starting node - * @return int The value of "table:number-rows-repeated" attribute of the current node, or 1 if attribute missing - */ - protected function getNumRowsRepeatedForCurrentNode($xmlReader) - { - $numRowsRepeated = $xmlReader->getAttribute(self::XML_ATTRIBUTE_NUM_ROWS_REPEATED); - return ($numRowsRepeated !== null) ? intval($numRowsRepeated) : 1; - } - - /** - * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "" starting node - * @return int The value of "table:number-columns-repeated" attribute of the current node, or 1 if attribute missing - */ - protected function getNumColumnsRepeatedForCurrentNode($xmlReader) - { - $numColumnsRepeated = $xmlReader->getAttribute(self::XML_ATTRIBUTE_NUM_COLUMNS_REPEATED); - return ($numColumnsRepeated !== null) ? intval($numColumnsRepeated) : 1; - } - - /** - * Returns the (unescaped) correctly marshalled, cell value associated to the given XML node. - * - * @param \DOMNode $node - * @return string|int|float|bool|\DateTime|\DateInterval|null The value associated with the cell, empty string if cell's type is void/undefined, null on error - */ - protected function getCellValue($node) - { - return $this->cellValueFormatter->extractAndFormatNodeValue($node); - } - - /** - * After finishing processing each cell, a row is considered empty if it contains - * no cells or if the value of the last read cell is an empty string. - * After finishing processing each cell, the last read cell is not part of the - * row data yet (as we still need to apply the "num-columns-repeated" attribute). - * - * @param array $rowData - * @param string|int|float|bool|\DateTime|\DateInterval|null The value of the last read cell - * @return bool Whether the row is empty - */ - protected function isEmptyRow($rowData, $lastReadCellValue) - { - return ( - count($rowData) === 0 && - (!isset($lastReadCellValue) || trim($lastReadCellValue) === '') - ); - } - - /** - * Return the current element, from the buffer. - * @link http://php.net/manual/en/iterator.current.php - * - * @return array|null - */ - public function current() - { - return $this->rowDataBuffer; - } - - /** - * Return the key of the current element - * @link http://php.net/manual/en/iterator.key.php - * - * @return int - */ - public function key() - { - return $this->lastRowIndexProcessed; - } - - - /** - * Cleans up what was created to iterate over the object. - * - * @return void - */ - public function end() - { - $this->xmlReader->close(); - } -} diff --git a/src/Spout/Reader/ODS/Sheet.php b/src/Spout/Reader/ODS/Sheet.php deleted file mode 100644 index 794ad3a..0000000 --- a/src/Spout/Reader/ODS/Sheet.php +++ /dev/null @@ -1,81 +0,0 @@ -" element - * @param int $sheetIndex Index of the sheet, based on order in the workbook (zero-based) - * @param string $sheetName Name of the sheet - * @param bool $isSheetActive Whether the sheet was defined as active - * @param \Box\Spout\Reader\ODS\ReaderOptions $options Reader's current options - */ - public function __construct($xmlReader, $sheetIndex, $sheetName, $isSheetActive, $options) - { - $this->rowIterator = new RowIterator($xmlReader, $options); - $this->index = $sheetIndex; - $this->name = $sheetName; - $this->isActive = $isSheetActive; - } - - /** - * @api - * @return \Box\Spout\Reader\ODS\RowIterator - */ - public function getRowIterator() - { - return $this->rowIterator; - } - - /** - * @api - * @return int Index of the sheet, based on order in the workbook (zero-based) - */ - public function getIndex() - { - return $this->index; - } - - /** - * @api - * @return string Name of the sheet - */ - public function getName() - { - return $this->name; - } - - /** - * @api - * @return bool Whether the sheet was defined as active - */ - public function isActive() - { - return $this->isActive; - } -} diff --git a/src/Spout/Reader/ODS/SheetIterator.php b/src/Spout/Reader/ODS/SheetIterator.php deleted file mode 100644 index 995c136..0000000 --- a/src/Spout/Reader/ODS/SheetIterator.php +++ /dev/null @@ -1,168 +0,0 @@ -filePath = $filePath; - $this->options = $options; - $this->xmlReader = new XMLReader(); - - /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ - $this->escaper = \Box\Spout\Common\Escaper\ODS::getInstance(); - - $settingsHelper = new SettingsHelper(); - $this->activeSheetName = $settingsHelper->getActiveSheetName($filePath); - } - - /** - * Rewind the Iterator to the first element - * @link http://php.net/manual/en/iterator.rewind.php - * - * @return void - * @throws \Box\Spout\Common\Exception\IOException If unable to open the XML file containing sheets' data - */ - public function rewind() - { - $this->xmlReader->close(); - - if ($this->xmlReader->openFileInZip($this->filePath, self::CONTENT_XML_FILE_PATH) === false) { - $contentXmlFilePath = $this->filePath . '#' . self::CONTENT_XML_FILE_PATH; - throw new IOException("Could not open \"{$contentXmlFilePath}\"."); - } - - try { - $this->hasFoundSheet = $this->xmlReader->readUntilNodeFound(self::XML_NODE_TABLE); - } catch (XMLProcessingException $exception) { - throw new IOException("The content.xml file is invalid and cannot be read. [{$exception->getMessage()}]"); - } - - $this->currentSheetIndex = 0; - } - - /** - * Checks if current position is valid - * @link http://php.net/manual/en/iterator.valid.php - * - * @return bool - */ - public function valid() - { - return $this->hasFoundSheet; - } - - /** - * Move forward to next element - * @link http://php.net/manual/en/iterator.next.php - * - * @return void - */ - public function next() - { - $this->hasFoundSheet = $this->xmlReader->readUntilNodeFound(self::XML_NODE_TABLE); - - if ($this->hasFoundSheet) { - $this->currentSheetIndex++; - } - } - - /** - * Return the current element - * @link http://php.net/manual/en/iterator.current.php - * - * @return \Box\Spout\Reader\ODS\Sheet - */ - public function current() - { - $escapedSheetName = $this->xmlReader->getAttribute(self::XML_ATTRIBUTE_TABLE_NAME); - $sheetName = $this->escaper->unescape($escapedSheetName); - $isActiveSheet = $this->isActiveSheet($sheetName, $this->currentSheetIndex, $this->activeSheetName); - - return new Sheet($this->xmlReader, $this->currentSheetIndex, $sheetName, $isActiveSheet, $this->options); - } - - /** - * Returns whether the current sheet was defined as the active one - * - * @param string $sheetName Name of the current sheet - * @param int $sheetIndex Index of the current sheet - * @param string|null Name of the sheet that was defined as active or NULL if none defined - * @return bool Whether the current sheet was defined as the active one - */ - private function isActiveSheet($sheetName, $sheetIndex, $activeSheetName) - { - // The given sheet is active if its name matches the defined active sheet's name - // or if no information about the active sheet was found, it defaults to the first sheet. - return ( - ($activeSheetName === null && $sheetIndex === 0) || - ($activeSheetName === $sheetName) - ); - } - - /** - * Return the key of the current element - * @link http://php.net/manual/en/iterator.key.php - * - * @return int - */ - public function key() - { - return $this->currentSheetIndex + 1; - } - - /** - * Cleans up what was created to iterate over the object. - * - * @return void - */ - public function end() - { - $this->xmlReader->close(); - } -} diff --git a/src/Spout/Reader/ReaderFactory.php b/src/Spout/Reader/ReaderFactory.php deleted file mode 100644 index 93a52cb..0000000 --- a/src/Spout/Reader/ReaderFactory.php +++ /dev/null @@ -1,48 +0,0 @@ -setGlobalFunctionsHelper(new GlobalFunctionsHelper()); - - return $reader; - } -} diff --git a/src/Spout/Reader/ReaderInterface.php b/src/Spout/Reader/ReaderInterface.php deleted file mode 100644 index 8ecde30..0000000 --- a/src/Spout/Reader/ReaderInterface.php +++ /dev/null @@ -1,36 +0,0 @@ -initialUseInternalErrorsValue = libxml_use_internal_errors(true); - } - - /** - * Throws an XMLProcessingException if an error occured. - * It also always resets the "libxml_use_internal_errors" setting back to its initial value. - * - * @return void - * @throws \Box\Spout\Reader\Exception\XMLProcessingException - */ - protected function resetXMLInternalErrorsSettingAndThrowIfXMLErrorOccured() - { - if ($this->hasXMLErrorOccured()) { - $this->resetXMLInternalErrorsSetting(); - throw new XMLProcessingException($this->getLastXMLErrorMessage()); - } - - $this->resetXMLInternalErrorsSetting(); - } - - /** - * Returns whether the a XML error has occured since the last time errors were cleared. - * - * @return bool TRUE if an error occured, FALSE otherwise - */ - private function hasXMLErrorOccured() - { - return (libxml_get_last_error() !== false); - } - - /** - * Returns the error message for the last XML error that occured. - * @see libxml_get_last_error - * - * @return String|null Last XML error message or null if no error - */ - private function getLastXMLErrorMessage() - { - $errorMessage = null; - $error = libxml_get_last_error(); - - if ($error !== false) { - $errorMessage = trim($error->message); - } - - return $errorMessage; - } - - /** - * @return void - */ - protected function resetXMLInternalErrorsSetting() - { - libxml_use_internal_errors($this->initialUseInternalErrorsValue); - } - -} diff --git a/src/Spout/Reader/Wrapper/XMLReader.php b/src/Spout/Reader/Wrapper/XMLReader.php deleted file mode 100644 index 2e20327..0000000 --- a/src/Spout/Reader/Wrapper/XMLReader.php +++ /dev/null @@ -1,167 +0,0 @@ -getRealPathURIForFileInZip($zipFilePath, $fileInsideZipPath); - - // We need to check first that the file we are trying to read really exist because: - // - PHP emits a warning when trying to open a file that does not exist. - // - HHVM does not check if file exists within zip file (@link https://github.com/facebook/hhvm/issues/5779) - if ($this->fileExistsWithinZip($realPathURI)) { - $wasOpenSuccessful = $this->open($realPathURI, null, LIBXML_NONET); - } - - return $wasOpenSuccessful; - } - - /** - * Returns the real path for the given path components. - * This is useful to avoid issues on some Windows setup. - * - * @param string $zipFilePath Path to the ZIP file - * @param string $fileInsideZipPath Relative or absolute path of the file inside the zip - * @return string The real path URI - */ - public function getRealPathURIForFileInZip($zipFilePath, $fileInsideZipPath) - { - return (self::ZIP_WRAPPER . realpath($zipFilePath) . '#' . $fileInsideZipPath); - } - - /** - * Returns whether the file at the given location exists - * - * @param string $zipStreamURI URI of a zip stream, e.g. "zip://file.zip#path/inside.xml" - * @return bool TRUE if the file exists, FALSE otherwise - */ - protected function fileExistsWithinZip($zipStreamURI) - { - $doesFileExists = false; - - $pattern = '/zip:\/\/([^#]+)#(.*)/'; - if (preg_match($pattern, $zipStreamURI, $matches)) { - $zipFilePath = $matches[1]; - $innerFilePath = $matches[2]; - - $zip = new \ZipArchive(); - if ($zip->open($zipFilePath) === true) { - $doesFileExists = ($zip->locateName($innerFilePath) !== false); - $zip->close(); - } - } - - return $doesFileExists; - } - - /** - * Move to next node in document - * @see \XMLReader::read - * - * @return bool TRUE on success or FALSE on failure - * @throws \Box\Spout\Reader\Exception\XMLProcessingException If an error/warning occurred - */ - public function read() - { - $this->useXMLInternalErrors(); - - $wasReadSuccessful = parent::read(); - - $this->resetXMLInternalErrorsSettingAndThrowIfXMLErrorOccured(); - - return $wasReadSuccessful; - } - - /** - * Read until the element with the given name is found, or the end of the file. - * - * @param string $nodeName Name of the node to find - * @return bool TRUE on success or FALSE on failure - * @throws \Box\Spout\Reader\Exception\XMLProcessingException If an error/warning occurred - */ - public function readUntilNodeFound($nodeName) - { - do { - $wasReadSuccessful = $this->read(); - $isNotPositionedOnStartingNode = !$this->isPositionedOnStartingNode($nodeName); - } while ($wasReadSuccessful && $isNotPositionedOnStartingNode); - - return $wasReadSuccessful; - } - - /** - * Move cursor to next node skipping all subtrees - * @see \XMLReader::next - * - * @param string|void $localName The name of the next node to move to - * @return bool TRUE on success or FALSE on failure - * @throws \Box\Spout\Reader\Exception\XMLProcessingException If an error/warning occurred - */ - public function next($localName = null) - { - $this->useXMLInternalErrors(); - - $wasNextSuccessful = parent::next($localName); - - $this->resetXMLInternalErrorsSettingAndThrowIfXMLErrorOccured(); - - return $wasNextSuccessful; - } - - /** - * @param string $nodeName - * @return bool Whether the XML Reader is currently positioned on the starting node with given name - */ - public function isPositionedOnStartingNode($nodeName) - { - return $this->isPositionedOnNode($nodeName, XMLReader::ELEMENT); - } - - /** - * @param string $nodeName - * @return bool Whether the XML Reader is currently positioned on the ending node with given name - */ - public function isPositionedOnEndingNode($nodeName) - { - return $this->isPositionedOnNode($nodeName, XMLReader::END_ELEMENT); - } - - /** - * @param string $nodeName - * @param int $nodeType - * @return bool Whether the XML Reader is currently positioned on the node with given name and type - */ - private function isPositionedOnNode($nodeName, $nodeType) - { - // In some cases, the node has a prefix (for instance, "" can also be ""). - // So if the given node name does not have a prefix, we need to look at the unprefixed name ("localName"). - // @see https://github.com/box/spout/issues/233 - $hasPrefix = (strpos($nodeName, ':') !== false); - $currentNodeName = ($hasPrefix) ? $this->name : $this->localName; - - return ($this->nodeType === $nodeType && $currentNodeName === $nodeName); - } -} diff --git a/src/Spout/Reader/XLSX/Helper/CellHelper.php b/src/Spout/Reader/XLSX/Helper/CellHelper.php deleted file mode 100644 index 6077839..0000000 --- a/src/Spout/Reader/XLSX/Helper/CellHelper.php +++ /dev/null @@ -1,106 +0,0 @@ - 0, 'B' => 1, 'C' => 2, 'D' => 3, 'E' => 4, 'F' => 5, 'G' => 6, - 'H' => 7, 'I' => 8, 'J' => 9, 'K' => 10, 'L' => 11, 'M' => 12, 'N' => 13, - 'O' => 14, 'P' => 15, 'Q' => 16, 'R' => 17, 'S' => 18, 'T' => 19, 'U' => 20, - 'V' => 21, 'W' => 22, 'X' => 23, 'Y' => 24, 'Z' => 25, - ]; - - /** - * Fills the missing indexes of an array with a given value. - * For instance, $dataArray = []; $a[1] = 1; $a[3] = 3; - * Calling fillMissingArrayIndexes($dataArray, 'FILL') will return this array: ['FILL', 1, 'FILL', 3] - * - * @param array $dataArray The array to fill - * @param string|void $fillValue optional - * @return array - */ - public static function fillMissingArrayIndexes($dataArray, $fillValue = '') - { - if (empty($dataArray)) { - return []; - } - $existingIndexes = array_keys($dataArray); - - $newIndexes = array_fill_keys(range(0, max($existingIndexes)), $fillValue); - $dataArray += $newIndexes; - - ksort($dataArray); - - return $dataArray; - } - - /** - * Returns the base 10 column index associated to the cell index (base 26). - * Excel uses A to Z letters for column indexing, where A is the 1st column, - * Z is the 26th and AA is the 27th. - * The mapping is zero based, so that A1 maps to 0, B2 maps to 1, Z13 to 25 and AA4 to 26. - * - * @param string $cellIndex The Excel cell index ('A1', 'BC13', ...) - * @return int - * @throws \Box\Spout\Common\Exception\InvalidArgumentException When the given cell index is invalid - */ - public static function getColumnIndexFromCellIndex($cellIndex) - { - if (!self::isValidCellIndex($cellIndex)) { - throw new InvalidArgumentException('Cannot get column index from an invalid cell index.'); - } - - $columnIndex = 0; - - // Remove row information - $columnLetters = preg_replace('/\d/', '', $cellIndex); - - // strlen() is super slow too... Using isset() is way faster and not too unreadable, - // since we checked before that there are between 1 and 3 letters. - $columnLength = isset($columnLetters[1]) ? (isset($columnLetters[2]) ? 3 : 2) : 1; - - // Looping over the different letters of the column is slower than this method. - // Also, not using the pow() function because it's slooooow... - switch ($columnLength) { - case 1: - $columnIndex = (self::$columnLetterToIndexMapping[$columnLetters]); - break; - case 2: - $firstLetterIndex = (self::$columnLetterToIndexMapping[$columnLetters[0]] + 1) * 26; - $secondLetterIndex = self::$columnLetterToIndexMapping[$columnLetters[1]]; - $columnIndex = $firstLetterIndex + $secondLetterIndex; - break; - case 3: - $firstLetterIndex = (self::$columnLetterToIndexMapping[$columnLetters[0]] + 1) * 676; - $secondLetterIndex = (self::$columnLetterToIndexMapping[$columnLetters[1]] + 1) * 26; - $thirdLetterIndex = self::$columnLetterToIndexMapping[$columnLetters[2]]; - $columnIndex = $firstLetterIndex + $secondLetterIndex + $thirdLetterIndex; - break; - } - - return $columnIndex; - } - - /** - * Returns whether a cell index is valid, in an Excel world. - * To be valid, the cell index should start with capital letters and be followed by numbers. - * There can only be 3 letters, as there can only be 16,384 rows, which is equivalent to 'XFE'. - * - * @param string $cellIndex The Excel cell index ('A1', 'BC13', ...) - * @return bool - */ - protected static function isValidCellIndex($cellIndex) - { - return (preg_match('/^[A-Z]{1,3}\d+$/', $cellIndex) === 1); - } -} diff --git a/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php b/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php deleted file mode 100644 index b4c6256..0000000 --- a/src/Spout/Reader/XLSX/Helper/CellValueFormatter.php +++ /dev/null @@ -1,300 +0,0 @@ -sharedStringsHelper = $sharedStringsHelper; - $this->styleHelper = $styleHelper; - $this->shouldFormatDates = $shouldFormatDates; - - /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ - $this->escaper = \Box\Spout\Common\Escaper\XLSX::getInstance(); - } - - /** - * Returns the (unescaped) correctly marshalled, cell value associated to the given XML node. - * - * @param \DOMNode $node - * @return string|int|float|bool|\DateTime|null The value associated with the cell (null when the cell has an error) - */ - public function extractAndFormatNodeValue($node) - { - // Default cell type is "n" - $cellType = $node->getAttribute(self::XML_ATTRIBUTE_TYPE) ?: self::CELL_TYPE_NUMERIC; - $cellStyleId = intval($node->getAttribute(self::XML_ATTRIBUTE_STYLE_ID)); - $vNodeValue = $this->getVNodeValue($node); - - if (($vNodeValue === '') && ($cellType !== self::CELL_TYPE_INLINE_STRING)) { - return $vNodeValue; - } - - switch ($cellType) { - case self::CELL_TYPE_INLINE_STRING: - return $this->formatInlineStringCellValue($node); - case self::CELL_TYPE_SHARED_STRING: - return $this->formatSharedStringCellValue($vNodeValue); - case self::CELL_TYPE_STR: - return $this->formatStrCellValue($vNodeValue); - case self::CELL_TYPE_BOOLEAN: - return $this->formatBooleanCellValue($vNodeValue); - case self::CELL_TYPE_NUMERIC: - return $this->formatNumericCellValue($vNodeValue, $cellStyleId); - case self::CELL_TYPE_DATE: - return $this->formatDateCellValue($vNodeValue); - default: - return null; - } - } - - /** - * Returns the cell's string value from a node's nested value node - * - * @param \DOMNode $node - * @return string The value associated with the cell - */ - protected function getVNodeValue($node) - { - // for cell types having a "v" tag containing the value. - // if not, the returned value should be empty string. - $vNode = $node->getElementsByTagName(self::XML_NODE_VALUE)->item(0); - return ($vNode !== null) ? $vNode->nodeValue : ''; - } - - /** - * Returns the cell String value where string is inline. - * - * @param \DOMNode $node - * @return string The value associated with the cell (null when the cell has an error) - */ - protected function formatInlineStringCellValue($node) - { - // inline strings are formatted this way: - // [INLINE_STRING] - $tNode = $node->getElementsByTagName(self::XML_NODE_INLINE_STRING_VALUE)->item(0); - $cellValue = $this->escaper->unescape($tNode->nodeValue); - return $cellValue; - } - - /** - * Returns the cell String value from shared-strings file using nodeValue index. - * - * @param string $nodeValue - * @return string The value associated with the cell (null when the cell has an error) - */ - protected function formatSharedStringCellValue($nodeValue) - { - // shared strings are formatted this way: - // [SHARED_STRING_INDEX] - $sharedStringIndex = intval($nodeValue); - $escapedCellValue = $this->sharedStringsHelper->getStringAtIndex($sharedStringIndex); - $cellValue = $this->escaper->unescape($escapedCellValue); - return $cellValue; - } - - /** - * Returns the cell String value, where string is stored in value node. - * - * @param string $nodeValue - * @return string The value associated with the cell (null when the cell has an error) - */ - protected function formatStrCellValue($nodeValue) - { - $escapedCellValue = trim($nodeValue); - $cellValue = $this->escaper->unescape($escapedCellValue); - return $cellValue; - } - - /** - * Returns the cell Numeric value from string of nodeValue. - * The value can also represent a timestamp and a DateTime will be returned. - * - * @param string $nodeValue - * @param int $cellStyleId 0 being the default style - * @return int|float|\DateTime|null The value associated with the cell - */ - protected function formatNumericCellValue($nodeValue, $cellStyleId) - { - // Numeric values can represent numbers as well as timestamps. - // We need to look at the style of the cell to determine whether it is one or the other. - $shouldFormatAsDate = $this->styleHelper->shouldFormatNumericValueAsDate($cellStyleId); - - if ($shouldFormatAsDate) { - return $this->formatExcelTimestampValue(floatval($nodeValue), $cellStyleId); - } else { - $nodeIntValue = intval($nodeValue); - return ($nodeIntValue == $nodeValue) ? $nodeIntValue : floatval($nodeValue); - } - } - - /** - * Returns a cell's PHP Date value, associated to the given timestamp. - * NOTE: The timestamp is a float representing the number of days since January 1st, 1900. - * NOTE: The timestamp can also represent a time, if it is a value between 0 and 1. - * - * @param float $nodeValue - * @param int $cellStyleId 0 being the default style - * @return \DateTime|null The value associated with the cell or NULL if invalid date value - */ - protected function formatExcelTimestampValue($nodeValue, $cellStyleId) - { - // Fix for the erroneous leap year in Excel - if (ceil($nodeValue) > self::ERRONEOUS_EXCEL_LEAP_YEAR_DAY) { - --$nodeValue; - } - - if ($nodeValue >= 1) { - // Values greater than 1 represent "dates". The value 1.0 representing the "base" date: 1900-01-01. - return $this->formatExcelTimestampValueAsDateValue($nodeValue, $cellStyleId); - } else if ($nodeValue >= 0) { - // Values between 0 and 1 represent "times". - return $this->formatExcelTimestampValueAsTimeValue($nodeValue, $cellStyleId); - } else { - // invalid date - return null; - } - } - - /** - * Returns a cell's PHP DateTime value, associated to the given timestamp. - * Only the time value matters. The date part is set to Jan 1st, 1900 (base Excel date). - * - * @param float $nodeValue - * @param int $cellStyleId 0 being the default style - * @return \DateTime|string The value associated with the cell - */ - protected function formatExcelTimestampValueAsTimeValue($nodeValue, $cellStyleId) - { - $time = round($nodeValue * self::NUM_SECONDS_IN_ONE_DAY); - $hours = floor($time / self::NUM_SECONDS_IN_ONE_HOUR); - $minutes = floor($time / self::NUM_SECONDS_IN_ONE_MINUTE) - ($hours * self::NUM_SECONDS_IN_ONE_MINUTE); - $seconds = $time - ($hours * self::NUM_SECONDS_IN_ONE_HOUR) - ($minutes * self::NUM_SECONDS_IN_ONE_MINUTE); - - // using the base Excel date (Jan 1st, 1900) - not relevant here - $dateObj = new \DateTime('1900-01-01'); - $dateObj->setTime($hours, $minutes, $seconds); - - if ($this->shouldFormatDates) { - $styleNumberFormatCode = $this->styleHelper->getNumberFormatCode($cellStyleId); - $phpDateFormat = DateFormatHelper::toPHPDateFormat($styleNumberFormatCode); - return $dateObj->format($phpDateFormat); - } else { - return $dateObj; - } - } - - /** - * Returns a cell's PHP Date value, associated to the given timestamp. - * NOTE: The timestamp is a float representing the number of days since January 1st, 1900. - * - * @param float $nodeValue - * @param int $cellStyleId 0 being the default style - * @return \DateTime|string|null The value associated with the cell or NULL if invalid date value - */ - protected function formatExcelTimestampValueAsDateValue($nodeValue, $cellStyleId) - { - // Do not use any unix timestamps for calculation to prevent - // issues with numbers exceeding 2^31. - $secondsRemainder = fmod($nodeValue, 1) * self::NUM_SECONDS_IN_ONE_DAY; - $secondsRemainder = round($secondsRemainder, 0); - - try { - $dateObj = \DateTime::createFromFormat('|Y-m-d', '1899-12-31'); - $dateObj->modify('+' . intval($nodeValue) . 'days'); - $dateObj->modify('+' . $secondsRemainder . 'seconds'); - - if ($this->shouldFormatDates) { - $styleNumberFormatCode = $this->styleHelper->getNumberFormatCode($cellStyleId); - $phpDateFormat = DateFormatHelper::toPHPDateFormat($styleNumberFormatCode); - return $dateObj->format($phpDateFormat); - } else { - return $dateObj; - } - } catch (\Exception $e) { - return null; - } - } - - /** - * Returns the cell Boolean value from a specific node's Value. - * - * @param string $nodeValue - * @return bool The value associated with the cell - */ - protected function formatBooleanCellValue($nodeValue) - { - // !! is similar to boolval() - $cellValue = !!$nodeValue; - return $cellValue; - } - - /** - * Returns a cell's PHP Date value, associated to the given stored nodeValue. - * @see ECMA-376 Part 1 - §18.17.4 - * - * @param string $nodeValue ISO 8601 Date string - * @return \DateTime|string|null The value associated with the cell or NULL if invalid date value - */ - protected function formatDateCellValue($nodeValue) - { - // Mitigate thrown Exception on invalid date-time format (http://php.net/manual/en/datetime.construct.php) - try { - return ($this->shouldFormatDates) ? $nodeValue : new \DateTime($nodeValue); - } catch (\Exception $e) { - return null; - } - } -} diff --git a/src/Spout/Reader/XLSX/Helper/DateFormatHelper.php b/src/Spout/Reader/XLSX/Helper/DateFormatHelper.php deleted file mode 100644 index 9dba4c6..0000000 --- a/src/Spout/Reader/XLSX/Helper/DateFormatHelper.php +++ /dev/null @@ -1,124 +0,0 @@ - [ - // Time - 'am/pm' => 'A', // Uppercase Ante meridiem and Post meridiem - ':mm' => ':i', // Minutes with leading zeros - if preceded by a ":" (otherwise month) - 'mm:' => 'i:', // Minutes with leading zeros - if followed by a ":" (otherwise month) - 'ss' => 's', // Seconds, with leading zeros - '.s' => '', // Ignore (fractional seconds format does not exist in PHP) - - // Date - 'e' => 'Y', // Full numeric representation of a year, 4 digits - 'yyyy' => 'Y', // Full numeric representation of a year, 4 digits - 'yy' => 'y', // Two digit representation of a year - 'mmmmm' => 'M', // Short textual representation of a month, three letters ("mmmmm" should only contain the 1st letter...) - 'mmmm' => 'F', // Full textual representation of a month - 'mmm' => 'M', // Short textual representation of a month, three letters - 'mm' => 'm', // Numeric representation of a month, with leading zeros - 'm' => 'n', // Numeric representation of a month, without leading zeros - 'dddd' => 'l', // Full textual representation of the day of the week - 'ddd' => 'D', // Textual representation of a day, three letters - 'dd' => 'd', // Day of the month, 2 digits with leading zeros - 'd' => 'j', // Day of the month without leading zeros - ], - self::KEY_HOUR_12 => [ - 'hh' => 'h', // 12-hour format of an hour without leading zeros - 'h' => 'g', // 12-hour format of an hour without leading zeros - ], - self::KEY_HOUR_24 => [ - 'hh' => 'H', // 24-hour hours with leading zero - 'h' => 'G', // 24-hour format of an hour without leading zeros - ], - ]; - - /** - * Converts the given Excel date format to a format understandable by the PHP date function. - * - * @param string $excelDateFormat Excel date format - * @return string PHP date format (as defined here: http://php.net/manual/en/function.date.php) - */ - public static function toPHPDateFormat($excelDateFormat) - { - // Remove brackets potentially present at the beginning of the format string - // and text portion of the format at the end of it (starting with ";") - // See §18.8.31 of ECMA-376 for more detail. - $dateFormat = preg_replace('/^(?:\[\$[^\]]+?\])?([^;]*).*/', '$1', $excelDateFormat); - - // Double quotes are used to escape characters that must not be interpreted. - // For instance, ["Day " dd] should result in "Day 13" and we should not try to interpret "D", "a", "y" - // By exploding the format string using double quote as a delimiter, we can get all parts - // that must be transformed (even indexes) and all parts that must not be (odd indexes). - $dateFormatParts = explode('"', $dateFormat); - - foreach ($dateFormatParts as $partIndex => $dateFormatPart) { - // do not look at odd indexes - if ($partIndex % 2 === 1) { - continue; - } - - // Make sure all characters are lowercase, as the mapping table is using lowercase characters - $transformedPart = strtolower($dateFormatPart); - - // Remove escapes related to non-format characters - $transformedPart = str_replace('\\', '', $transformedPart); - - // Apply general transformation first... - $transformedPart = strtr($transformedPart, self::$excelDateFormatToPHPDateFormatMapping[self::KEY_GENERAL]); - - // ... then apply hour transformation, for 12-hour or 24-hour format - if (self::has12HourFormatMarker($dateFormatPart)) { - $transformedPart = strtr($transformedPart, self::$excelDateFormatToPHPDateFormatMapping[self::KEY_HOUR_12]); - } else { - $transformedPart = strtr($transformedPart, self::$excelDateFormatToPHPDateFormatMapping[self::KEY_HOUR_24]); - } - - // overwrite the parts array with the new transformed part - $dateFormatParts[$partIndex] = $transformedPart; - } - - // Merge all transformed parts back together - $phpDateFormat = implode('"', $dateFormatParts); - - // Finally, to have the date format compatible with the DateTime::format() function, we need to escape - // all characters that are inside double quotes (and double quotes must be removed). - // For instance, ["Day " dd] should become [\D\a\y\ dd] - $phpDateFormat = preg_replace_callback('/"(.+?)"/', function($matches) { - $stringToEscape = $matches[1]; - $letters = preg_split('//u', $stringToEscape, -1, PREG_SPLIT_NO_EMPTY); - return '\\' . implode('\\', $letters); - }, $phpDateFormat); - - return $phpDateFormat; - } - - /** - * @param string $excelDateFormat Date format as defined by Excel - * @return bool Whether the given date format has the 12-hour format marker - */ - private static function has12HourFormatMarker($excelDateFormat) - { - return (stripos($excelDateFormat, 'am/pm') !== false); - } -} diff --git a/src/Spout/Reader/XLSX/Helper/SharedStringsCaching/CachingStrategyFactory.php b/src/Spout/Reader/XLSX/Helper/SharedStringsCaching/CachingStrategyFactory.php deleted file mode 100644 index 36e0bfe..0000000 --- a/src/Spout/Reader/XLSX/Helper/SharedStringsCaching/CachingStrategyFactory.php +++ /dev/null @@ -1,159 +0,0 @@ - 20 * 600 ≈ 12KB - */ - const AMOUNT_MEMORY_NEEDED_PER_STRING_IN_KB = 12; - - /** - * To avoid running out of memory when extracting a huge number of shared strings, they can be saved to temporary files - * instead of in memory. Then, when accessing a string, the corresponding file contents will be loaded in memory - * and the string will be quickly retrieved. - * The performance bottleneck is not when creating these temporary files, but rather when loading their content. - * Because the contents of the last loaded file stays in memory until another file needs to be loaded, it works - * best when the indexes of the shared strings are sorted in the sheet data. - * 10,000 was chosen because it creates small files that are fast to be loaded in memory. - */ - const MAX_NUM_STRINGS_PER_TEMP_FILE = 10000; - - /** @var CachingStrategyFactory|null Singleton instance */ - protected static $instance = null; - - /** - * Private constructor for singleton - */ - private function __construct() - { - } - - /** - * Returns the singleton instance of the factory - * - * @return CachingStrategyFactory - */ - public static function getInstance() - { - if (self::$instance === null) { - self::$instance = new CachingStrategyFactory(); - } - - return self::$instance; - } - - /** - * Returns the best caching strategy, given the number of unique shared strings - * and the amount of memory available. - * - * @param int|null $sharedStringsUniqueCount Number of unique shared strings (NULL if unknown) - * @param string|void $tempFolder Temporary folder where the temporary files to store shared strings will be stored - * @return CachingStrategyInterface The best caching strategy - */ - public function getBestCachingStrategy($sharedStringsUniqueCount, $tempFolder = null) - { - if ($this->isInMemoryStrategyUsageSafe($sharedStringsUniqueCount)) { - return new InMemoryStrategy($sharedStringsUniqueCount); - } else { - return new FileBasedStrategy($tempFolder, self::MAX_NUM_STRINGS_PER_TEMP_FILE); - } - } - - /** - * Returns whether it is safe to use in-memory caching, given the number of unique shared strings - * and the amount of memory available. - * - * @param int|null $sharedStringsUniqueCount Number of unique shared strings (NULL if unknown) - * @return bool - */ - protected function isInMemoryStrategyUsageSafe($sharedStringsUniqueCount) - { - // if the number of shared strings in unknown, do not use "in memory" strategy - if ($sharedStringsUniqueCount === null) { - return false; - } - - $memoryAvailable = $this->getMemoryLimitInKB(); - - if ($memoryAvailable === -1) { - // if cannot get memory limit or if memory limit set as unlimited, don't trust and play safe - return ($sharedStringsUniqueCount < self::MAX_NUM_STRINGS_PER_TEMP_FILE); - } else { - $memoryNeeded = $sharedStringsUniqueCount * self::AMOUNT_MEMORY_NEEDED_PER_STRING_IN_KB; - return ($memoryAvailable > $memoryNeeded); - } - } - - /** - * Returns the PHP "memory_limit" in Kilobytes - * - * @return float - */ - protected function getMemoryLimitInKB() - { - $memoryLimitFormatted = $this->getMemoryLimitFromIni(); - $memoryLimitFormatted = strtolower(trim($memoryLimitFormatted)); - - // No memory limit - if ($memoryLimitFormatted === '-1') { - return -1; - } - - if (preg_match('/(\d+)([bkmgt])b?/', $memoryLimitFormatted, $matches)) { - $amount = intval($matches[1]); - $unit = $matches[2]; - - switch ($unit) { - case 'b': return ($amount / 1024); - case 'k': return $amount; - case 'm': return ($amount * 1024); - case 'g': return ($amount * 1024 * 1024); - case 't': return ($amount * 1024 * 1024 * 1024); - } - } - - return -1; - } - - /** - * Returns the formatted "memory_limit" value - * - * @return string - */ - protected function getMemoryLimitFromIni() - { - return ini_get('memory_limit'); - } -} diff --git a/src/Spout/Reader/XLSX/Helper/SharedStringsCaching/CachingStrategyInterface.php b/src/Spout/Reader/XLSX/Helper/SharedStringsCaching/CachingStrategyInterface.php deleted file mode 100644 index 631222a..0000000 --- a/src/Spout/Reader/XLSX/Helper/SharedStringsCaching/CachingStrategyInterface.php +++ /dev/null @@ -1,44 +0,0 @@ -fileSystemHelper = new FileSystemHelper($rootTempFolder); - $this->tempFolder = $this->fileSystemHelper->createFolder($rootTempFolder, uniqid('sharedstrings')); - - $this->maxNumStringsPerTempFile = $maxNumStringsPerTempFile; - - $this->globalFunctionsHelper = new GlobalFunctionsHelper(); - $this->tempFilePointer = null; - } - - /** - * Adds the given string to the cache. - * - * @param string $sharedString The string to be added to the cache - * @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file - * @return void - */ - public function addStringForIndex($sharedString, $sharedStringIndex) - { - $tempFilePath = $this->getSharedStringTempFilePath($sharedStringIndex); - - if (!$this->globalFunctionsHelper->file_exists($tempFilePath)) { - if ($this->tempFilePointer) { - $this->globalFunctionsHelper->fclose($this->tempFilePointer); - } - $this->tempFilePointer = $this->globalFunctionsHelper->fopen($tempFilePath, 'w'); - } - - // The shared string retrieval logic expects each cell data to be on one line only - // Encoding the line feed character allows to preserve this assumption - $lineFeedEncodedSharedString = $this->escapeLineFeed($sharedString); - - $this->globalFunctionsHelper->fwrite($this->tempFilePointer, $lineFeedEncodedSharedString . PHP_EOL); - } - - /** - * Returns the path for the temp file that should contain the string for the given index - * - * @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file - * @return string The temp file path for the given index - */ - protected function getSharedStringTempFilePath($sharedStringIndex) - { - $numTempFile = intval($sharedStringIndex / $this->maxNumStringsPerTempFile); - return $this->tempFolder . '/sharedstrings' . $numTempFile; - } - - /** - * Closes the cache after the last shared string was added. - * This prevents any additional string from being added to the cache. - * - * @return void - */ - public function closeCache() - { - // close pointer to the last temp file that was written - if ($this->tempFilePointer) { - $this->globalFunctionsHelper->fclose($this->tempFilePointer); - } - } - - - /** - * Returns the string located at the given index from the cache. - * - * @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file - * @return string The shared string at the given index - * @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If no shared string found for the given index - */ - public function getStringAtIndex($sharedStringIndex) - { - $tempFilePath = $this->getSharedStringTempFilePath($sharedStringIndex); - $indexInFile = $sharedStringIndex % $this->maxNumStringsPerTempFile; - - if (!$this->globalFunctionsHelper->file_exists($tempFilePath)) { - throw new SharedStringNotFoundException("Shared string temp file not found: $tempFilePath ; for index: $sharedStringIndex"); - } - - if ($this->inMemoryTempFilePath !== $tempFilePath) { - // free memory - unset($this->inMemoryTempFileContents); - - $this->inMemoryTempFileContents = explode(PHP_EOL, $this->globalFunctionsHelper->file_get_contents($tempFilePath)); - $this->inMemoryTempFilePath = $tempFilePath; - } - - $sharedString = null; - - // Using isset here because it is way faster than array_key_exists... - if (isset($this->inMemoryTempFileContents[$indexInFile])) { - $escapedSharedString = $this->inMemoryTempFileContents[$indexInFile]; - $sharedString = $this->unescapeLineFeed($escapedSharedString); - } - - if ($sharedString === null) { - throw new SharedStringNotFoundException("Shared string not found for index: $sharedStringIndex"); - } - - return rtrim($sharedString, PHP_EOL); - } - - /** - * Escapes the line feed characters (\n) - * - * @param string $unescapedString - * @return string - */ - private function escapeLineFeed($unescapedString) - { - return str_replace("\n", self::ESCAPED_LINE_FEED_CHARACTER, $unescapedString); - } - - /** - * Unescapes the line feed characters (\n) - * - * @param string $escapedString - * @return string - */ - private function unescapeLineFeed($escapedString) - { - return str_replace(self::ESCAPED_LINE_FEED_CHARACTER, "\n", $escapedString); - } - - /** - * Destroys the cache, freeing memory and removing any created artifacts - * - * @return void - */ - public function clearCache() - { - if ($this->tempFolder) { - $this->fileSystemHelper->deleteFolderRecursively($this->tempFolder); - } - } -} diff --git a/src/Spout/Reader/XLSX/Helper/SharedStringsCaching/InMemoryStrategy.php b/src/Spout/Reader/XLSX/Helper/SharedStringsCaching/InMemoryStrategy.php deleted file mode 100644 index c6a5321..0000000 --- a/src/Spout/Reader/XLSX/Helper/SharedStringsCaching/InMemoryStrategy.php +++ /dev/null @@ -1,83 +0,0 @@ -inMemoryCache = new \SplFixedArray($sharedStringsUniqueCount); - $this->isCacheClosed = false; - } - - /** - * Adds the given string to the cache. - * - * @param string $sharedString The string to be added to the cache - * @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file - * @return void - */ - public function addStringForIndex($sharedString, $sharedStringIndex) - { - if (!$this->isCacheClosed) { - $this->inMemoryCache->offsetSet($sharedStringIndex, $sharedString); - } - } - - /** - * Closes the cache after the last shared string was added. - * This prevents any additional string from being added to the cache. - * - * @return void - */ - public function closeCache() - { - $this->isCacheClosed = true; - } - - /** - * Returns the string located at the given index from the cache. - * - * @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file - * @return string The shared string at the given index - * @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If no shared string found for the given index - */ - public function getStringAtIndex($sharedStringIndex) - { - try { - return $this->inMemoryCache->offsetGet($sharedStringIndex); - } catch (\RuntimeException $e) { - throw new SharedStringNotFoundException("Shared string not found for index: $sharedStringIndex"); - } - } - - /** - * Destroys the cache, freeing memory and removing any created artifacts - * - * @return void - */ - public function clearCache() - { - unset($this->inMemoryCache); - $this->isCacheClosed = false; - } -} diff --git a/src/Spout/Reader/XLSX/Helper/SharedStringsHelper.php b/src/Spout/Reader/XLSX/Helper/SharedStringsHelper.php deleted file mode 100644 index 0e88839..0000000 --- a/src/Spout/Reader/XLSX/Helper/SharedStringsHelper.php +++ /dev/null @@ -1,245 +0,0 @@ -filePath = $filePath; - $this->tempFolder = $tempFolder; - } - - /** - * Returns whether the XLSX file contains a shared strings XML file - * - * @return bool - */ - public function hasSharedStrings() - { - $hasSharedStrings = false; - $zip = new \ZipArchive(); - - if ($zip->open($this->filePath) === true) { - $hasSharedStrings = ($zip->locateName(self::SHARED_STRINGS_XML_FILE_PATH) !== false); - $zip->close(); - } - - return $hasSharedStrings; - } - - /** - * Builds an in-memory array containing all the shared strings of the sheet. - * All the strings are stored in a XML file, located at 'xl/sharedStrings.xml'. - * It is then accessed by the sheet data, via the string index in the built table. - * - * More documentation available here: http://msdn.microsoft.com/en-us/library/office/gg278314.aspx - * - * The XML file can be really big with sheets containing a lot of data. That is why - * we need to use a XML reader that provides streaming like the XMLReader library. - * Please note that SimpleXML does not provide such a functionality but since it is faster - * and more handy to parse few XML nodes, it is used in combination with XMLReader for that purpose. - * - * @return void - * @throws \Box\Spout\Common\Exception\IOException If sharedStrings.xml can't be read - */ - public function extractSharedStrings() - { - $xmlReader = new XMLReader(); - $sharedStringIndex = 0; - - $sharedStringsFilePath = $this->getSharedStringsFilePath(); - if ($xmlReader->open($sharedStringsFilePath) === false) { - throw new IOException('Could not open "' . self::SHARED_STRINGS_XML_FILE_PATH . '".'); - } - - try { - $sharedStringsUniqueCount = $this->getSharedStringsUniqueCount($xmlReader); - $this->cachingStrategy = $this->getBestSharedStringsCachingStrategy($sharedStringsUniqueCount); - - $xmlReader->readUntilNodeFound(self::XML_NODE_SI); - - while ($xmlReader->name === self::XML_NODE_SI) { - $this->processSharedStringsItem($xmlReader, $sharedStringIndex); - $sharedStringIndex++; - - // jump to the next '' tag - $xmlReader->next(self::XML_NODE_SI); - } - - $this->cachingStrategy->closeCache(); - - } catch (XMLProcessingException $exception) { - throw new IOException("The sharedStrings.xml file is invalid and cannot be read. [{$exception->getMessage()}]"); - } - - $xmlReader->close(); - } - - /** - * @return string The path to the shared strings XML file - */ - protected function getSharedStringsFilePath() - { - return 'zip://' . $this->filePath . '#' . self::SHARED_STRINGS_XML_FILE_PATH; - } - - /** - * Returns the shared strings unique count, as specified in tag. - * - * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader instance - * @return int|null Number of unique shared strings in the sharedStrings.xml file - * @throws \Box\Spout\Common\Exception\IOException If sharedStrings.xml is invalid and can't be read - */ - protected function getSharedStringsUniqueCount($xmlReader) - { - $xmlReader->next(self::XML_NODE_SST); - - // Iterate over the "sst" elements to get the actual "sst ELEMENT" (skips any DOCTYPE) - while ($xmlReader->name === self::XML_NODE_SST && $xmlReader->nodeType !== XMLReader::ELEMENT) { - $xmlReader->read(); - } - - $uniqueCount = $xmlReader->getAttribute(self::XML_ATTRIBUTE_UNIQUE_COUNT); - - // some software do not add the "uniqueCount" attribute but only use the "count" one - // @see https://github.com/box/spout/issues/254 - if ($uniqueCount === null) { - $uniqueCount = $xmlReader->getAttribute(self::XML_ATTRIBUTE_COUNT); - } - - return ($uniqueCount !== null) ? intval($uniqueCount) : null; - } - - /** - * Returns the best shared strings caching strategy. - * - * @param int|null $sharedStringsUniqueCount Number of unique shared strings (NULL if unknown) - * @return CachingStrategyInterface - */ - protected function getBestSharedStringsCachingStrategy($sharedStringsUniqueCount) - { - return CachingStrategyFactory::getInstance() - ->getBestCachingStrategy($sharedStringsUniqueCount, $this->tempFolder); - } - - /** - * Processes the shared strings item XML node which the given XML reader is positioned on. - * - * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XML Reader positioned on a "" node - * @param int $sharedStringIndex Index of the processed shared strings item - * @return void - */ - protected function processSharedStringsItem($xmlReader, $sharedStringIndex) - { - $sharedStringValue = ''; - - // NOTE: expand() will automatically decode all XML entities of the child nodes - $siNode = $xmlReader->expand(); - $textNodes = $siNode->getElementsByTagName(self::XML_NODE_T); - - foreach ($textNodes as $textNode) { - if ($this->shouldExtractTextNodeValue($textNode)) { - $textNodeValue = $textNode->nodeValue; - $shouldPreserveWhitespace = $this->shouldPreserveWhitespace($textNode); - - $sharedStringValue .= ($shouldPreserveWhitespace) ? $textNodeValue : trim($textNodeValue); - } - } - - $this->cachingStrategy->addStringForIndex($sharedStringValue, $sharedStringIndex); - } - - /** - * Not all text nodes' values must be extracted. - * Some text nodes are part of a node describing the pronunciation for instance. - * We'll only consider the nodes whose parents are "" or "". - * - * @param \DOMElement $textNode Text node to check - * @return bool Whether the given text node's value must be extracted - */ - protected function shouldExtractTextNodeValue($textNode) - { - $parentTagName = $textNode->parentNode->localName; - return ($parentTagName === self::XML_NODE_SI || $parentTagName === self::XML_NODE_R); - } - - /** - * If the text node has the attribute 'xml:space="preserve"', then preserve whitespace. - * - * @param \DOMElement $textNode The text node element () whose whitespace may be preserved - * @return bool Whether whitespace should be preserved - */ - protected function shouldPreserveWhitespace($textNode) - { - $spaceValue = $textNode->getAttribute(self::XML_ATTRIBUTE_XML_SPACE); - return ($spaceValue === self::XML_ATTRIBUTE_VALUE_PRESERVE); - } - - /** - * Returns the shared string at the given index, using the previously chosen caching strategy. - * - * @param int $sharedStringIndex Index of the shared string in the sharedStrings.xml file - * @return string The shared string at the given index - * @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If no shared string found for the given index - */ - public function getStringAtIndex($sharedStringIndex) - { - return $this->cachingStrategy->getStringAtIndex($sharedStringIndex); - } - - /** - * Destroys the cache, freeing memory and removing any created artifacts - * - * @return void - */ - public function cleanup() - { - if ($this->cachingStrategy) { - $this->cachingStrategy->clearCache(); - } - } -} diff --git a/src/Spout/Reader/XLSX/Helper/SheetHelper.php b/src/Spout/Reader/XLSX/Helper/SheetHelper.php deleted file mode 100644 index b74ba01..0000000 --- a/src/Spout/Reader/XLSX/Helper/SheetHelper.php +++ /dev/null @@ -1,156 +0,0 @@ -filePath = $filePath; - $this->options = $options; - $this->sharedStringsHelper = $sharedStringsHelper; - $this->globalFunctionsHelper = $globalFunctionsHelper; - } - - /** - * Returns the sheets metadata of the file located at the previously given file path. - * The paths to the sheets' data are read from the [Content_Types].xml file. - * - * @return Sheet[] Sheets within the XLSX file - */ - public function getSheets() - { - $sheets = []; - $sheetIndex = 0; - $activeSheetIndex = 0; // By default, the first sheet is active - - $xmlReader = new XMLReader(); - if ($xmlReader->openFileInZip($this->filePath, self::WORKBOOK_XML_FILE_PATH)) { - while ($xmlReader->read()) { - if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_WORKBOOK_VIEW)) { - // The "workbookView" node is located before "sheet" nodes, ensuring that - // the active sheet is known before parsing sheets data. - $activeSheetIndex = (int) $xmlReader->getAttribute(self::XML_ATTRIBUTE_ACTIVE_TAB); - } else if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_SHEET)) { - $isSheetActive = ($sheetIndex === $activeSheetIndex); - $sheets[] = $this->getSheetFromSheetXMLNode($xmlReader, $sheetIndex, $isSheetActive); - $sheetIndex++; - } else if ($xmlReader->isPositionedOnEndingNode(self::XML_NODE_SHEETS)) { - // stop reading once all sheets have been read - break; - } - } - - $xmlReader->close(); - } - - return $sheets; - } - - /** - * Returns an instance of a sheet, given the XML node describing the sheet - from "workbook.xml". - * We can find the XML file path describing the sheet inside "workbook.xml.res", by mapping with the sheet ID - * ("r:id" in "workbook.xml", "Id" in "workbook.xml.res"). - * - * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReaderOnSheetNode XML Reader instance, pointing on the node describing the sheet, as defined in "workbook.xml" - * @param int $sheetIndexZeroBased Index of the sheet, based on order of appearance in the workbook (zero-based) - * @param bool $isSheetActive Whether this sheet was defined as active - * @return \Box\Spout\Reader\XLSX\Sheet Sheet instance - */ - protected function getSheetFromSheetXMLNode($xmlReaderOnSheetNode, $sheetIndexZeroBased, $isSheetActive) - { - $sheetId = $xmlReaderOnSheetNode->getAttribute(self::XML_ATTRIBUTE_R_ID); - $escapedSheetName = $xmlReaderOnSheetNode->getAttribute(self::XML_ATTRIBUTE_NAME); - - /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ - $escaper = \Box\Spout\Common\Escaper\XLSX::getInstance(); - $sheetName = $escaper->unescape($escapedSheetName); - - $sheetDataXMLFilePath = $this->getSheetDataXMLFilePathForSheetId($sheetId); - - return new Sheet( - $this->filePath, $sheetDataXMLFilePath, - $sheetIndexZeroBased, $sheetName, $isSheetActive, - $this->options, $this->sharedStringsHelper - ); - } - - /** - * @param string $sheetId The sheet ID, as defined in "workbook.xml" - * @return string The XML file path describing the sheet inside "workbook.xml.res", for the given sheet ID - */ - protected function getSheetDataXMLFilePathForSheetId($sheetId) - { - $sheetDataXMLFilePath = ''; - - // find the file path of the sheet, by looking at the "workbook.xml.res" file - $xmlReader = new XMLReader(); - if ($xmlReader->openFileInZip($this->filePath, self::WORKBOOK_XML_RELS_FILE_PATH)) { - while ($xmlReader->read()) { - if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_RELATIONSHIP)) { - $relationshipSheetId = $xmlReader->getAttribute(self::XML_ATTRIBUTE_ID); - - if ($relationshipSheetId === $sheetId) { - // In workbook.xml.rels, it is only "worksheets/sheet1.xml" - // In [Content_Types].xml, the path is "/xl/worksheets/sheet1.xml" - $sheetDataXMLFilePath = $xmlReader->getAttribute(self::XML_ATTRIBUTE_TARGET); - - // sometimes, the sheet data file path already contains "/xl/"... - if (strpos($sheetDataXMLFilePath, '/xl/') !== 0) { - $sheetDataXMLFilePath = '/xl/' . $sheetDataXMLFilePath; - break; - } - } - } - } - - $xmlReader->close(); - } - - return $sheetDataXMLFilePath; - } -} diff --git a/src/Spout/Reader/XLSX/Helper/StyleHelper.php b/src/Spout/Reader/XLSX/Helper/StyleHelper.php deleted file mode 100644 index 000adab..0000000 --- a/src/Spout/Reader/XLSX/Helper/StyleHelper.php +++ /dev/null @@ -1,330 +0,0 @@ - 'm/d/yyyy', // @NOTE: ECMA spec is 'mm-dd-yy' - 15 => 'd-mmm-yy', - 16 => 'd-mmm', - 17 => 'mmm-yy', - 18 => 'h:mm AM/PM', - 19 => 'h:mm:ss AM/PM', - 20 => 'h:mm', - 21 => 'h:mm:ss', - 22 => 'm/d/yyyy h:mm', // @NOTE: ECMA spec is 'm/d/yy h:mm', - 45 => 'mm:ss', - 46 => '[h]:mm:ss', - 47 => 'mm:ss.0', // @NOTE: ECMA spec is 'mmss.0', - ]; - - /** @var string Path of the XLSX file being read */ - protected $filePath; - - /** @var array Array containing the IDs of built-in number formats indicating a date */ - protected $builtinNumFmtIdIndicatingDates; - - /** @var array Array containing a mapping NUM_FMT_ID => FORMAT_CODE */ - protected $customNumberFormats; - - /** @var array Array containing a mapping STYLE_ID => [STYLE_ATTRIBUTES] */ - protected $stylesAttributes; - - /** @var array Cache containing a mapping NUM_FMT_ID => IS_DATE_FORMAT. Used to avoid lots of recalculations */ - protected $numFmtIdToIsDateFormatCache = []; - - /** - * @param string $filePath Path of the XLSX file being read - */ - public function __construct($filePath) - { - $this->filePath = $filePath; - $this->builtinNumFmtIdIndicatingDates = array_keys(self::$builtinNumFmtIdToNumFormatMapping); - } - - /** - * Returns whether the style with the given ID should consider - * numeric values as timestamps and format the cell as a date. - * - * @param int $styleId Zero-based style ID - * @return bool Whether the cell with the given cell should display a date instead of a numeric value - */ - public function shouldFormatNumericValueAsDate($styleId) - { - $stylesAttributes = $this->getStylesAttributes(); - - // Default style (0) does not format numeric values as timestamps. Only custom styles do. - // Also if the style ID does not exist in the styles.xml file, format as numeric value. - // Using isset here because it is way faster than array_key_exists... - if ($styleId === self::DEFAULT_STYLE_ID || !isset($stylesAttributes[$styleId])) { - return false; - } - - $styleAttributes = $stylesAttributes[$styleId]; - - return $this->doesStyleIndicateDate($styleAttributes); - } - - /** - * Reads the styles.xml file and extract the relevant information from the file. - * - * @return void - */ - protected function extractRelevantInfo() - { - $this->customNumberFormats = []; - $this->stylesAttributes = []; - - $xmlReader = new XMLReader(); - - if ($xmlReader->openFileInZip($this->filePath, self::STYLES_XML_FILE_PATH)) { - while ($xmlReader->read()) { - if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_NUM_FMTS)) { - $this->extractNumberFormats($xmlReader); - - } else if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_CELL_XFS)) { - $this->extractStyleAttributes($xmlReader); - } - } - - $xmlReader->close(); - } - } - - /** - * Extracts number formats from the "numFmt" nodes. - * For simplicity, the styles attributes are kept in memory. This is possible thanks - * to the reuse of formats. So 1 million cells should not use 1 million formats. - * - * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XML Reader positioned on the "numFmts" node - * @return void - */ - protected function extractNumberFormats($xmlReader) - { - while ($xmlReader->read()) { - if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_NUM_FMT)) { - $numFmtId = intval($xmlReader->getAttribute(self::XML_ATTRIBUTE_NUM_FMT_ID)); - $formatCode = $xmlReader->getAttribute(self::XML_ATTRIBUTE_FORMAT_CODE); - $this->customNumberFormats[$numFmtId] = $formatCode; - } else if ($xmlReader->isPositionedOnEndingNode(self::XML_NODE_NUM_FMTS)) { - // Once done reading "numFmts" node's children - break; - } - } - } - - /** - * Extracts style attributes from the "xf" nodes, inside the "cellXfs" section. - * For simplicity, the styles attributes are kept in memory. This is possible thanks - * to the reuse of styles. So 1 million cells should not use 1 million styles. - * - * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XML Reader positioned on the "cellXfs" node - * @return void - */ - protected function extractStyleAttributes($xmlReader) - { - while ($xmlReader->read()) { - if ($xmlReader->isPositionedOnStartingNode(self::XML_NODE_XF)) { - $numFmtId = $xmlReader->getAttribute(self::XML_ATTRIBUTE_NUM_FMT_ID); - $normalizedNumFmtId = ($numFmtId !== null) ? intval($numFmtId) : null; - - $applyNumberFormat = $xmlReader->getAttribute(self::XML_ATTRIBUTE_APPLY_NUMBER_FORMAT); - $normalizedApplyNumberFormat = ($applyNumberFormat !== null) ? !!$applyNumberFormat : null; - - $this->stylesAttributes[] = [ - self::XML_ATTRIBUTE_NUM_FMT_ID => $normalizedNumFmtId, - self::XML_ATTRIBUTE_APPLY_NUMBER_FORMAT => $normalizedApplyNumberFormat, - ]; - } else if ($xmlReader->isPositionedOnEndingNode(self::XML_NODE_CELL_XFS)) { - // Once done reading "cellXfs" node's children - break; - } - } - } - - /** - * @return array The custom number formats - */ - protected function getCustomNumberFormats() - { - if (!isset($this->customNumberFormats)) { - $this->extractRelevantInfo(); - } - - return $this->customNumberFormats; - } - - /** - * @return array The styles attributes - */ - protected function getStylesAttributes() - { - if (!isset($this->stylesAttributes)) { - $this->extractRelevantInfo(); - } - - return $this->stylesAttributes; - } - - /** - * @param array $styleAttributes Array containing the style attributes (2 keys: "applyNumberFormat" and "numFmtId") - * @return bool Whether the style with the given attributes indicates that the number is a date - */ - protected function doesStyleIndicateDate($styleAttributes) - { - $applyNumberFormat = $styleAttributes[self::XML_ATTRIBUTE_APPLY_NUMBER_FORMAT]; - $numFmtId = $styleAttributes[self::XML_ATTRIBUTE_NUM_FMT_ID]; - - // A style may apply a date format if it has: - // - "applyNumberFormat" attribute not set to "false" - // - "numFmtId" attribute set - // This is a preliminary check, as having "numFmtId" set just means the style should apply a specific number format, - // but this is not necessarily a date. - if ($applyNumberFormat === false || $numFmtId === null) { - return false; - } - - return $this->doesNumFmtIdIndicateDate($numFmtId); - } - - /** - * Returns whether the number format ID indicates that the number is a date. - * The result is cached to avoid recomputing the same thing over and over, as - * "numFmtId" attributes can be shared between multiple styles. - * - * @param int $numFmtId - * @return bool Whether the number format ID indicates that the number is a date - */ - protected function doesNumFmtIdIndicateDate($numFmtId) - { - if (!isset($this->numFmtIdToIsDateFormatCache[$numFmtId])) { - $formatCode = $this->getFormatCodeForNumFmtId($numFmtId); - - $this->numFmtIdToIsDateFormatCache[$numFmtId] = ( - $this->isNumFmtIdBuiltInDateFormat($numFmtId) || - $this->isFormatCodeCustomDateFormat($formatCode) - ); - } - - return $this->numFmtIdToIsDateFormatCache[$numFmtId]; - } - - /** - * @param int $numFmtId - * @return string|null The custom number format or NULL if none defined for the given numFmtId - */ - protected function getFormatCodeForNumFmtId($numFmtId) - { - $customNumberFormats = $this->getCustomNumberFormats(); - - // Using isset here because it is way faster than array_key_exists... - return (isset($customNumberFormats[$numFmtId])) ? $customNumberFormats[$numFmtId] : null; - } - - /** - * @param int $numFmtId - * @return bool Whether the number format ID indicates that the number is a date - */ - protected function isNumFmtIdBuiltInDateFormat($numFmtId) - { - return in_array($numFmtId, $this->builtinNumFmtIdIndicatingDates); - } - - /** - * @param string|null $formatCode - * @return bool Whether the given format code indicates that the number is a date - */ - protected function isFormatCodeCustomDateFormat($formatCode) - { - // if no associated format code or if using the default "General" format - if ($formatCode === null || strcasecmp($formatCode, self::NUMBER_FORMAT_GENERAL) === 0) { - return false; - } - - return $this->isFormatCodeMatchingDateFormatPattern($formatCode); - } - - /** - * @param string $formatCode - * @return bool Whether the given format code matches a date format pattern - */ - protected function isFormatCodeMatchingDateFormatPattern($formatCode) - { - // Remove extra formatting (what's between [ ], the brackets should not be preceded by a "\") - $pattern = '((?getStylesAttributes(); - $styleAttributes = $stylesAttributes[$styleId]; - $numFmtId = $styleAttributes[self::XML_ATTRIBUTE_NUM_FMT_ID]; - - if ($this->isNumFmtIdBuiltInDateFormat($numFmtId)) { - $numberFormatCode = self::$builtinNumFmtIdToNumFormatMapping[$numFmtId]; - } else { - $customNumberFormats = $this->getCustomNumberFormats(); - $numberFormatCode = $customNumberFormats[$numFmtId]; - } - - return $numberFormatCode; - } -} diff --git a/src/Spout/Reader/XLSX/Reader.php b/src/Spout/Reader/XLSX/Reader.php deleted file mode 100644 index 76e8e32..0000000 --- a/src/Spout/Reader/XLSX/Reader.php +++ /dev/null @@ -1,113 +0,0 @@ -options)) { - $this->options = new ReaderOptions(); - } - return $this->options; - } - - /** - * @param string $tempFolder Temporary folder where the temporary files will be created - * @return Reader - */ - public function setTempFolder($tempFolder) - { - $this->getOptions()->setTempFolder($tempFolder); - return $this; - } - - /** - * Returns whether stream wrappers are supported - * - * @return bool - */ - protected function doesSupportStreamWrapper() - { - return false; - } - - /** - * Opens the file at the given file path to make it ready to be read. - * It also parses the sharedStrings.xml file to get all the shared strings available in memory - * and fetches all the available sheets. - * - * @param string $filePath Path of the file to be read - * @return void - * @throws \Box\Spout\Common\Exception\IOException If the file at the given path or its content cannot be read - * @throws \Box\Spout\Reader\Exception\NoSheetsFoundException If there are no sheets in the file - */ - protected function openReader($filePath) - { - $this->zip = new \ZipArchive(); - - if ($this->zip->open($filePath) === true) { - $this->sharedStringsHelper = new SharedStringsHelper($filePath, $this->getOptions()->getTempFolder()); - - if ($this->sharedStringsHelper->hasSharedStrings()) { - // Extracts all the strings from the sheets for easy access in the future - $this->sharedStringsHelper->extractSharedStrings(); - } - - $this->sheetIterator = new SheetIterator($filePath, $this->getOptions(), $this->sharedStringsHelper, $this->globalFunctionsHelper); - } else { - throw new IOException("Could not open $filePath for reading."); - } - } - - /** - * Returns an iterator to iterate over sheets. - * - * @return SheetIterator To iterate over sheets - */ - protected function getConcreteSheetIterator() - { - return $this->sheetIterator; - } - - /** - * Closes the reader. To be used after reading the file. - * - * @return void - */ - protected function closeReader() - { - if ($this->zip) { - $this->zip->close(); - } - - if ($this->sharedStringsHelper) { - $this->sharedStringsHelper->cleanup(); - } - } -} diff --git a/src/Spout/Reader/XLSX/ReaderOptions.php b/src/Spout/Reader/XLSX/ReaderOptions.php deleted file mode 100644 index 5f78c5d..0000000 --- a/src/Spout/Reader/XLSX/ReaderOptions.php +++ /dev/null @@ -1,33 +0,0 @@ -tempFolder; - } - - /** - * @param string|null $tempFolder Temporary folder where the temporary files will be created - * @return ReaderOptions - */ - public function setTempFolder($tempFolder) - { - $this->tempFolder = $tempFolder; - return $this; - } -} diff --git a/src/Spout/Reader/XLSX/RowIterator.php b/src/Spout/Reader/XLSX/RowIterator.php deleted file mode 100644 index 2440593..0000000 --- a/src/Spout/Reader/XLSX/RowIterator.php +++ /dev/null @@ -1,406 +0,0 @@ -filePath = $filePath; - $this->sheetDataXMLFilePath = $this->normalizeSheetDataXMLFilePath($sheetDataXMLFilePath); - - $this->xmlReader = new XMLReader(); - - $this->styleHelper = new StyleHelper($filePath); - $this->cellValueFormatter = new CellValueFormatter($sharedStringsHelper, $this->styleHelper, $options->shouldFormatDates()); - - $this->shouldPreserveEmptyRows = $options->shouldPreserveEmptyRows(); - - // Register all callbacks to process different nodes when reading the XML file - $this->xmlProcessor = new XMLProcessor($this->xmlReader); - $this->xmlProcessor->registerCallback(self::XML_NODE_DIMENSION, XMLProcessor::NODE_TYPE_START, [$this, 'processDimensionStartingNode']); - $this->xmlProcessor->registerCallback(self::XML_NODE_ROW, XMLProcessor::NODE_TYPE_START, [$this, 'processRowStartingNode']); - $this->xmlProcessor->registerCallback(self::XML_NODE_CELL, XMLProcessor::NODE_TYPE_START, [$this, 'processCellStartingNode']); - $this->xmlProcessor->registerCallback(self::XML_NODE_ROW, XMLProcessor::NODE_TYPE_END, [$this, 'processRowEndingNode']); - $this->xmlProcessor->registerCallback(self::XML_NODE_WORKSHEET, XMLProcessor::NODE_TYPE_END, [$this, 'processWorksheetEndingNode']); - } - - /** - * @param string $sheetDataXMLFilePath Path of the sheet data XML file as in [Content_Types].xml - * @return string Path of the XML file containing the sheet data, - * without the leading slash. - */ - protected function normalizeSheetDataXMLFilePath($sheetDataXMLFilePath) - { - return ltrim($sheetDataXMLFilePath, '/'); - } - - /** - * Rewind the Iterator to the first element. - * Initializes the XMLReader object that reads the associated sheet data. - * The XMLReader is configured to be safe from billion laughs attack. - * @link http://php.net/manual/en/iterator.rewind.php - * - * @return void - * @throws \Box\Spout\Common\Exception\IOException If the sheet data XML cannot be read - */ - public function rewind() - { - $this->xmlReader->close(); - - $sheetDataFilePath = 'zip://' . $this->filePath . '#' . $this->sheetDataXMLFilePath; - if ($this->xmlReader->open($sheetDataFilePath) === false) { - throw new IOException("Could not open \"{$this->sheetDataXMLFilePath}\"."); - } - - $this->numReadRows = 0; - $this->lastRowIndexProcessed = 0; - $this->nextRowIndexToBeProcessed = 0; - $this->rowDataBuffer = null; - $this->hasReachedEndOfFile = false; - $this->numColumns = 0; - - $this->next(); - } - - /** - * Checks if current position is valid - * @link http://php.net/manual/en/iterator.valid.php - * - * @return bool - */ - public function valid() - { - return (!$this->hasReachedEndOfFile); - } - - /** - * Move forward to next element. Reads data describing the next unprocessed row. - * @link http://php.net/manual/en/iterator.next.php - * - * @return void - * @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If a shared string was not found - * @throws \Box\Spout\Common\Exception\IOException If unable to read the sheet data XML - */ - public function next() - { - $this->nextRowIndexToBeProcessed++; - - if ($this->doesNeedDataForNextRowToBeProcessed()) { - $this->readDataForNextRow(); - } - } - - /** - * Returns whether we need data for the next row to be processed. - * We don't need to read data if: - * we have already read at least one row - * AND - * we need to preserve empty rows - * AND - * the last row that was read is not the row that need to be processed - * (i.e. if we need to return empty rows) - * - * @return bool Whether we need data for the next row to be processed. - */ - protected function doesNeedDataForNextRowToBeProcessed() - { - $hasReadAtLeastOneRow = ($this->lastRowIndexProcessed !== 0); - - return ( - !$hasReadAtLeastOneRow || - !$this->shouldPreserveEmptyRows || - $this->lastRowIndexProcessed < $this->nextRowIndexToBeProcessed - ); - } - - /** - * @return void - * @throws \Box\Spout\Reader\Exception\SharedStringNotFoundException If a shared string was not found - * @throws \Box\Spout\Common\Exception\IOException If unable to read the sheet data XML - */ - protected function readDataForNextRow() - { - $this->currentlyProcessedRowData = []; - - try { - $this->xmlProcessor->readUntilStopped(); - } catch (XMLProcessingException $exception) { - throw new IOException("The {$this->sheetDataXMLFilePath} file cannot be read. [{$exception->getMessage()}]"); - } - - $this->rowDataBuffer = $this->currentlyProcessedRowData; - } - - /** - * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "" starting node - * @return int A return code that indicates what action should the processor take next - */ - protected function processDimensionStartingNode($xmlReader) - { - // Read dimensions of the sheet - $dimensionRef = $xmlReader->getAttribute(self::XML_ATTRIBUTE_REF); // returns 'A1:M13' for instance (or 'A1' for empty sheet) - if (preg_match('/[A-Z]+\d+:([A-Z]+\d+)/', $dimensionRef, $matches)) { - $this->numColumns = CellHelper::getColumnIndexFromCellIndex($matches[1]) + 1; - } - - return XMLProcessor::PROCESSING_CONTINUE; - } - - /** - * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "" starting node - * @return int A return code that indicates what action should the processor take next - */ - protected function processRowStartingNode($xmlReader) - { - // Reset index of the last processed column - $this->lastColumnIndexProcessed = -1; - - // Mark the last processed row as the one currently being read - $this->lastRowIndexProcessed = $this->getRowIndex($xmlReader); - - // Read spans info if present - $numberOfColumnsForRow = $this->numColumns; - $spans = $xmlReader->getAttribute(self::XML_ATTRIBUTE_SPANS); // returns '1:5' for instance - if ($spans) { - list(, $numberOfColumnsForRow) = explode(':', $spans); - $numberOfColumnsForRow = intval($numberOfColumnsForRow); - } - - $this->currentlyProcessedRowData = ($numberOfColumnsForRow !== 0) ? array_fill(0, $numberOfColumnsForRow, '') : []; - - return XMLProcessor::PROCESSING_CONTINUE; - } - - /** - * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "" starting node - * @return int A return code that indicates what action should the processor take next - */ - protected function processCellStartingNode($xmlReader) - { - $currentColumnIndex = $this->getColumnIndex($xmlReader); - - // NOTE: expand() will automatically decode all XML entities of the child nodes - $node = $xmlReader->expand(); - $this->currentlyProcessedRowData[$currentColumnIndex] = $this->getCellValue($node); - $this->lastColumnIndexProcessed = $currentColumnIndex; - - return XMLProcessor::PROCESSING_CONTINUE; - } - - /** - * @return int A return code that indicates what action should the processor take next - */ - protected function processRowEndingNode() - { - // if the fetched row is empty and we don't want to preserve it.., - if (!$this->shouldPreserveEmptyRows && $this->isEmptyRow($this->currentlyProcessedRowData)) { - // ... skip it - return XMLProcessor::PROCESSING_CONTINUE; - } - - $this->numReadRows++; - - // If needed, we fill the empty cells - if ($this->numColumns === 0) { - $this->currentlyProcessedRowData = CellHelper::fillMissingArrayIndexes($this->currentlyProcessedRowData); - } - - // at this point, we have all the data we need for the row - // so that we can populate the buffer - return XMLProcessor::PROCESSING_STOP; - } - - /** - * @return int A return code that indicates what action should the processor take next - */ - protected function processWorksheetEndingNode() - { - // The closing "" marks the end of the file - $this->hasReachedEndOfFile = true; - - return XMLProcessor::PROCESSING_STOP; - } - - /** - * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "" node - * @return int Row index - * @throws \Box\Spout\Common\Exception\InvalidArgumentException When the given cell index is invalid - */ - protected function getRowIndex($xmlReader) - { - // Get "r" attribute if present (from something like - $currentRowIndex = $xmlReader->getAttribute(self::XML_ATTRIBUTE_ROW_INDEX); - - return ($currentRowIndex !== null) ? - intval($currentRowIndex) : - $this->lastRowIndexProcessed + 1; - } - - /** - * @param \Box\Spout\Reader\Wrapper\XMLReader $xmlReader XMLReader object, positioned on a "" node - * @return int Column index - * @throws \Box\Spout\Common\Exception\InvalidArgumentException When the given cell index is invalid - */ - protected function getColumnIndex($xmlReader) - { - // Get "r" attribute if present (from something like - $currentCellIndex = $xmlReader->getAttribute(self::XML_ATTRIBUTE_CELL_INDEX); - - return ($currentCellIndex !== null) ? - CellHelper::getColumnIndexFromCellIndex($currentCellIndex) : - $this->lastColumnIndexProcessed + 1; - } - - /** - * Returns the (unescaped) correctly marshalled, cell value associated to the given XML node. - * - * @param \DOMNode $node - * @return string|int|float|bool|\DateTime|null The value associated with the cell (null when the cell has an error) - */ - protected function getCellValue($node) - { - return $this->cellValueFormatter->extractAndFormatNodeValue($node); - } - - /** - * @param array $rowData - * @return bool Whether the given row is empty - */ - protected function isEmptyRow($rowData) - { - return (count($rowData) === 1 && $rowData[0] === ''); - } - - /** - * Return the current element, either an empty row or from the buffer. - * @link http://php.net/manual/en/iterator.current.php - * - * @return array|null - */ - public function current() - { - $rowDataForRowToBeProcessed = $this->rowDataBuffer; - - if ($this->shouldPreserveEmptyRows) { - // when we need to preserve empty rows, we will either return - // an empty row or the last row read. This depends whether the - // index of last row that was read matches the index of the last - // row whose value should be returned. - if ($this->lastRowIndexProcessed !== $this->nextRowIndexToBeProcessed) { - // return empty row if mismatch between last processed row - // and the row that needs to be returned - $rowDataForRowToBeProcessed = ['']; - } - } - - return $rowDataForRowToBeProcessed; - } - - /** - * Return the key of the current element. Here, the row index. - * @link http://php.net/manual/en/iterator.key.php - * - * @return int - */ - public function key() - { - // TODO: This should return $this->nextRowIndexToBeProcessed - // but to avoid a breaking change, the return value for - // this function has been kept as the number of rows read. - return $this->shouldPreserveEmptyRows ? - $this->nextRowIndexToBeProcessed : - $this->numReadRows; - } - - - /** - * Cleans up what was created to iterate over the object. - * - * @return void - */ - public function end() - { - $this->xmlReader->close(); - } -} diff --git a/src/Spout/Reader/XLSX/Sheet.php b/src/Spout/Reader/XLSX/Sheet.php deleted file mode 100644 index 9baaef2..0000000 --- a/src/Spout/Reader/XLSX/Sheet.php +++ /dev/null @@ -1,79 +0,0 @@ -rowIterator = new RowIterator($filePath, $sheetDataXMLFilePath, $options, $sharedStringsHelper); - $this->index = $sheetIndex; - $this->name = $sheetName; - $this->isActive = $isSheetActive; - } - - /** - * @api - * @return \Box\Spout\Reader\XLSX\RowIterator - */ - public function getRowIterator() - { - return $this->rowIterator; - } - - /** - * @api - * @return int Index of the sheet, based on order in the workbook (zero-based) - */ - public function getIndex() - { - return $this->index; - } - - /** - * @api - * @return string Name of the sheet - */ - public function getName() - { - return $this->name; - } - - /** - * @api - * @return bool Whether the sheet was defined as active - */ - public function isActive() - { - return $this->isActive; - } -} diff --git a/src/Spout/Reader/XLSX/SheetIterator.php b/src/Spout/Reader/XLSX/SheetIterator.php deleted file mode 100644 index 7ba07d3..0000000 --- a/src/Spout/Reader/XLSX/SheetIterator.php +++ /dev/null @@ -1,114 +0,0 @@ -sheets = $sheetHelper->getSheets(); - - if (count($this->sheets) === 0) { - throw new NoSheetsFoundException('The file must contain at least one sheet.'); - } - } - - /** - * Rewind the Iterator to the first element - * @link http://php.net/manual/en/iterator.rewind.php - * - * @return void - */ - public function rewind() - { - $this->currentSheetIndex = 0; - } - - /** - * Checks if current position is valid - * @link http://php.net/manual/en/iterator.valid.php - * - * @return bool - */ - public function valid() - { - return ($this->currentSheetIndex < count($this->sheets)); - } - - /** - * Move forward to next element - * @link http://php.net/manual/en/iterator.next.php - * - * @return void - */ - public function next() - { - // Using isset here because it is way faster than array_key_exists... - if (isset($this->sheets[$this->currentSheetIndex])) { - $currentSheet = $this->sheets[$this->currentSheetIndex]; - $currentSheet->getRowIterator()->end(); - - $this->currentSheetIndex++; - } - } - - /** - * Return the current element - * @link http://php.net/manual/en/iterator.current.php - * - * @return \Box\Spout\Reader\XLSX\Sheet - */ - public function current() - { - return $this->sheets[$this->currentSheetIndex]; - } - - /** - * Return the key of the current element - * @link http://php.net/manual/en/iterator.key.php - * - * @return int - */ - public function key() - { - return $this->currentSheetIndex + 1; - } - - /** - * Cleans up what was created to iterate over the object. - * - * @return void - */ - public function end() - { - // make sure we are not leaking memory in case the iteration stopped before the end - foreach ($this->sheets as $sheet) { - $sheet->getRowIterator()->end(); - } - } -} diff --git a/src/Spout/Writer/AbstractMultiSheetsWriter.php b/src/Spout/Writer/AbstractMultiSheetsWriter.php deleted file mode 100644 index d119660..0000000 --- a/src/Spout/Writer/AbstractMultiSheetsWriter.php +++ /dev/null @@ -1,119 +0,0 @@ -throwIfWriterAlreadyOpened('Writer must be configured before opening it.'); - - $this->shouldCreateNewSheetsAutomatically = $shouldCreateNewSheetsAutomatically; - return $this; - } - - /** - * Returns all the workbook's sheets - * - * @api - * @return Common\Sheet[] All the workbook's sheets - * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the writer has not been opened yet - */ - public function getSheets() - { - $this->throwIfBookIsNotAvailable(); - - $externalSheets = []; - $worksheets = $this->getWorkbook()->getWorksheets(); - - /** @var Common\Internal\WorksheetInterface $worksheet */ - foreach ($worksheets as $worksheet) { - $externalSheets[] = $worksheet->getExternalSheet(); - } - - return $externalSheets; - } - - /** - * Creates a new sheet and make it the current sheet. The data will now be written to this sheet. - * - * @api - * @return Common\Sheet The created sheet - * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the writer has not been opened yet - */ - public function addNewSheetAndMakeItCurrent() - { - $this->throwIfBookIsNotAvailable(); - $worksheet = $this->getWorkbook()->addNewSheetAndMakeItCurrent(); - - return $worksheet->getExternalSheet(); - } - - /** - * Returns the current sheet - * - * @api - * @return Common\Sheet The current sheet - * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the writer has not been opened yet - */ - public function getCurrentSheet() - { - $this->throwIfBookIsNotAvailable(); - return $this->getWorkbook()->getCurrentWorksheet()->getExternalSheet(); - } - - /** - * Sets the given sheet as the current one. New data will be written to this sheet. - * The writing will resume where it stopped (i.e. data won't be truncated). - * - * @api - * @param Common\Sheet $sheet The sheet to set as current - * @return void - * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the writer has not been opened yet - * @throws \Box\Spout\Writer\Exception\SheetNotFoundException If the given sheet does not exist in the workbook - */ - public function setCurrentSheet($sheet) - { - $this->throwIfBookIsNotAvailable(); - $this->getWorkbook()->setCurrentSheet($sheet); - } - - /** - * Checks if the book has been created. Throws an exception if not created yet. - * - * @return void - * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the book is not created yet - */ - protected function throwIfBookIsNotAvailable() - { - if (!$this->getWorkbook()) { - throw new WriterNotOpenedException('The writer must be opened before performing this action.'); - } - } -} - diff --git a/src/Spout/Writer/AbstractWriter.php b/src/Spout/Writer/AbstractWriter.php deleted file mode 100644 index 83e190f..0000000 --- a/src/Spout/Writer/AbstractWriter.php +++ /dev/null @@ -1,384 +0,0 @@ -defaultRowStyle = $this->getDefaultRowStyle(); - $this->resetRowStyleToDefault(); - } - - /** - * Sets the default styles for all rows added with "addRow". - * Overriding the default style instead of using "addRowWithStyle" improves performance by 20%. - * @see https://github.com/box/spout/issues/272 - * - * @param Style\Style $defaultStyle - * @return AbstractWriter - */ - public function setDefaultRowStyle($defaultStyle) - { - $this->defaultRowStyle = $defaultStyle; - $this->resetRowStyleToDefault(); - return $this; - } - - /** - * @param \Box\Spout\Common\Helper\GlobalFunctionsHelper $globalFunctionsHelper - * @return AbstractWriter - */ - public function setGlobalFunctionsHelper($globalFunctionsHelper) - { - $this->globalFunctionsHelper = $globalFunctionsHelper; - return $this; - } - - /** - * Inits the writer and opens it to accept data. - * By using this method, the data will be written to a file. - * - * @api - * @param string $outputFilePath Path of the output file that will contain the data - * @return AbstractWriter - * @throws \Box\Spout\Common\Exception\IOException If the writer cannot be opened or if the given path is not writable - */ - public function openToFile($outputFilePath) - { - $this->outputFilePath = $outputFilePath; - - $this->filePointer = $this->globalFunctionsHelper->fopen($this->outputFilePath, 'wb+'); - $this->throwIfFilePointerIsNotAvailable(); - - $this->openWriter(); - $this->isWriterOpened = true; - - return $this; - } - - /** - * Inits the writer and opens it to accept data. - * By using this method, the data will be outputted directly to the browser. - * - * @codeCoverageIgnore - * - * @api - * @param string $outputFileName Name of the output file that will contain the data. If a path is passed in, only the file name will be kept - * @return AbstractWriter - * @throws \Box\Spout\Common\Exception\IOException If the writer cannot be opened - */ - public function openToBrowser($outputFileName) - { - $this->outputFilePath = $this->globalFunctionsHelper->basename($outputFileName); - - $this->filePointer = $this->globalFunctionsHelper->fopen('php://output', 'w'); - $this->throwIfFilePointerIsNotAvailable(); - - // Clear any previous output (otherwise the generated file will be corrupted) - // @see https://github.com/box/spout/issues/241 - $this->globalFunctionsHelper->ob_end_clean(); - - // Set headers - $this->globalFunctionsHelper->header('Content-Type: ' . static::$headerContentType); - $this->globalFunctionsHelper->header('Content-Disposition: attachment; filename="' . $this->outputFilePath . '"'); - - /* - * When forcing the download of a file over SSL,IE8 and lower browsers fail - * if the Cache-Control and Pragma headers are not set. - * - * @see http://support.microsoft.com/KB/323308 - * @see https://github.com/liuggio/ExcelBundle/issues/45 - */ - $this->globalFunctionsHelper->header('Cache-Control: max-age=0'); - $this->globalFunctionsHelper->header('Pragma: public'); - - $this->openWriter(); - $this->isWriterOpened = true; - - return $this; - } - - /** - * Checks if the pointer to the file/stream to write to is available. - * Will throw an exception if not available. - * - * @return void - * @throws \Box\Spout\Common\Exception\IOException If the pointer is not available - */ - protected function throwIfFilePointerIsNotAvailable() - { - if (!$this->filePointer) { - throw new IOException('File pointer has not be opened'); - } - } - - /** - * Checks if the writer has already been opened, since some actions must be done before it gets opened. - * Throws an exception if already opened. - * - * @param string $message Error message - * @return void - * @throws \Box\Spout\Writer\Exception\WriterAlreadyOpenedException If the writer was already opened and must not be. - */ - protected function throwIfWriterAlreadyOpened($message) - { - if ($this->isWriterOpened) { - throw new WriterAlreadyOpenedException($message); - } - } - - /** - * Write given data to the output. New data will be appended to end of stream. - * - * @param array $dataRow Array containing data to be streamed. - * If empty, no data is added (i.e. not even as a blank row) - * Example: $dataRow = ['data1', 1234, null, '', 'data5', false]; - * @api - * @return AbstractWriter - * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If this function is called before opening the writer - * @throws \Box\Spout\Common\Exception\IOException If unable to write data - * @throws \Box\Spout\Common\Exception\SpoutException If anything else goes wrong while writing data - */ - public function addRow(array $dataRow) - { - if ($this->isWriterOpened) { - // empty $dataRow should not add an empty line - if (!empty($dataRow)) { - try { - $this->addRowToWriter($dataRow, $this->rowStyle); - } catch (SpoutException $e) { - // if an exception occurs while writing data, - // close the writer and remove all files created so far. - $this->closeAndAttemptToCleanupAllFiles(); - - // re-throw the exception to alert developers of the error - throw $e; - } - } - } else { - throw new WriterNotOpenedException('The writer needs to be opened before adding row.'); - } - - return $this; - } - - /** - * Write given data to the output and apply the given style. - * @see addRow - * - * @api - * @param array $dataRow Array of array containing data to be streamed. - * @param Style\Style $style Style to be applied to the row. - * @return AbstractWriter - * @throws \Box\Spout\Common\Exception\InvalidArgumentException If the input param is not valid - * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If this function is called before opening the writer - * @throws \Box\Spout\Common\Exception\IOException If unable to write data - */ - public function addRowWithStyle(array $dataRow, $style) - { - if (!$style instanceof Style\Style) { - throw new InvalidArgumentException('The "$style" argument must be a Style instance and cannot be NULL.'); - } - - $this->setRowStyle($style); - $this->addRow($dataRow); - $this->resetRowStyleToDefault(); - - return $this; - } - - /** - * Write given data to the output. New data will be appended to end of stream. - * - * @api - * @param array $dataRows Array of array containing data to be streamed. - * If a row is empty, it won't be added (i.e. not even as a blank row) - * Example: $dataRows = [ - * ['data11', 12, , '', 'data13'], - * ['data21', 'data22', null, false], - * ]; - * @return AbstractWriter - * @throws \Box\Spout\Common\Exception\InvalidArgumentException If the input param is not valid - * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If this function is called before opening the writer - * @throws \Box\Spout\Common\Exception\IOException If unable to write data - */ - public function addRows(array $dataRows) - { - if (!empty($dataRows)) { - $firstRow = reset($dataRows); - if (!is_array($firstRow)) { - throw new InvalidArgumentException('The input should be an array of arrays'); - } - - foreach ($dataRows as $dataRow) { - $this->addRow($dataRow); - } - } - - return $this; - } - - /** - * Write given data to the output and apply the given style. - * @see addRows - * - * @api - * @param array $dataRows Array of array containing data to be streamed. - * @param Style\Style $style Style to be applied to the rows. - * @return AbstractWriter - * @throws \Box\Spout\Common\Exception\InvalidArgumentException If the input param is not valid - * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If this function is called before opening the writer - * @throws \Box\Spout\Common\Exception\IOException If unable to write data - */ - public function addRowsWithStyle(array $dataRows, $style) - { - if (!$style instanceof Style\Style) { - throw new InvalidArgumentException('The "$style" argument must be a Style instance and cannot be NULL.'); - } - - $this->setRowStyle($style); - $this->addRows($dataRows); - $this->resetRowStyleToDefault(); - - return $this; - } - - /** - * Returns the default style to be applied to rows. - * Can be overriden by children to have a custom style. - * - * @return Style\Style - */ - protected function getDefaultRowStyle() - { - return (new StyleBuilder())->build(); - } - - /** - * Sets the style to be applied to the next written rows - * until it is changed or reset. - * - * @param Style\Style $style - * @return void - */ - private function setRowStyle($style) - { - // Merge given style with the default one to inherit custom properties - $this->rowStyle = $style->mergeWith($this->defaultRowStyle); - } - - /** - * Resets the style to be applied to the next written rows. - * - * @return void - */ - private function resetRowStyleToDefault() - { - $this->rowStyle = $this->defaultRowStyle; - } - - /** - * Closes the writer. This will close the streamer as well, preventing new data - * to be written to the file. - * - * @api - * @return void - */ - public function close() - { - if (!$this->isWriterOpened) { - return; - } - - $this->closeWriter(); - - if (is_resource($this->filePointer)) { - $this->globalFunctionsHelper->fclose($this->filePointer); - } - - $this->isWriterOpened = false; - } - - /** - * Closes the writer and attempts to cleanup all files that were - * created during the writing process (temp files & final file). - * - * @return void - */ - private function closeAndAttemptToCleanupAllFiles() - { - // close the writer, which should remove all temp files - $this->close(); - - // remove output file if it was created - if ($this->globalFunctionsHelper->file_exists($this->outputFilePath)) { - $outputFolderPath = dirname($this->outputFilePath); - $fileSystemHelper = new FileSystemHelper($outputFolderPath); - $fileSystemHelper->deleteFile($this->outputFilePath); - } - } -} diff --git a/src/Spout/Writer/CSV/Writer.php b/src/Spout/Writer/CSV/Writer.php deleted file mode 100644 index f7f1fda..0000000 --- a/src/Spout/Writer/CSV/Writer.php +++ /dev/null @@ -1,118 +0,0 @@ -fieldDelimiter = $fieldDelimiter; - return $this; - } - - /** - * Sets the field enclosure for the CSV - * - * @api - * @param string $fieldEnclosure Character that enclose fields - * @return Writer - */ - public function setFieldEnclosure($fieldEnclosure) - { - $this->fieldEnclosure = $fieldEnclosure; - return $this; - } - - /** - * Set if a BOM has to be added to the file - * - * @param bool $shouldAddBOM - * @return Writer - */ - public function setShouldAddBOM($shouldAddBOM) - { - $this->shouldAddBOM = (bool) $shouldAddBOM; - return $this; - } - - /** - * Opens the CSV streamer and makes it ready to accept data. - * - * @return void - */ - protected function openWriter() - { - if ($this->shouldAddBOM) { - // Adds UTF-8 BOM for Unicode compatibility - $this->globalFunctionsHelper->fputs($this->filePointer, EncodingHelper::BOM_UTF8); - } - } - - /** - * Adds data to the currently opened writer. - * - * @param array $dataRow Array containing data to be written. - * Example $dataRow = ['data1', 1234, null, '', 'data5']; - * @param \Box\Spout\Writer\Style\Style $style Ignored here since CSV does not support styling. - * @return void - * @throws \Box\Spout\Common\Exception\IOException If unable to write data - */ - protected function addRowToWriter(array $dataRow, $style) - { - $wasWriteSuccessful = $this->globalFunctionsHelper->fputcsv($this->filePointer, $dataRow, $this->fieldDelimiter, $this->fieldEnclosure); - if ($wasWriteSuccessful === false) { - throw new IOException('Unable to write data'); - } - - $this->lastWrittenRowIndex++; - if ($this->lastWrittenRowIndex % self::FLUSH_THRESHOLD === 0) { - $this->globalFunctionsHelper->fflush($this->filePointer); - } - } - - /** - * Closes the CSV streamer, preventing any additional writing. - * If set, sets the headers and redirects output to the browser. - * - * @return void - */ - protected function closeWriter() - { - $this->lastWrittenRowIndex = 0; - } -} diff --git a/src/Spout/Writer/Common/Helper/AbstractStyleHelper.php b/src/Spout/Writer/Common/Helper/AbstractStyleHelper.php deleted file mode 100644 index fa5e267..0000000 --- a/src/Spout/Writer/Common/Helper/AbstractStyleHelper.php +++ /dev/null @@ -1,138 +0,0 @@ - [STYLE_ID] mapping table, keeping track of the registered styles */ - protected $serializedStyleToStyleIdMappingTable = []; - - /** @var array [STYLE_ID] => [STYLE] mapping table, keeping track of the registered styles */ - protected $styleIdToStyleMappingTable = []; - - /** - * @param \Box\Spout\Writer\Style\Style $defaultStyle - */ - public function __construct($defaultStyle) - { - // This ensures that the default style is the first one to be registered - $this->registerStyle($defaultStyle); - } - - /** - * Registers the given style as a used style. - * Duplicate styles won't be registered more than once. - * - * @param \Box\Spout\Writer\Style\Style $style The style to be registered - * @return \Box\Spout\Writer\Style\Style The registered style, updated with an internal ID. - */ - public function registerStyle($style) - { - $serializedStyle = $style->serialize(); - - if (!$this->hasStyleAlreadyBeenRegistered($style)) { - $nextStyleId = count($this->serializedStyleToStyleIdMappingTable); - $style->setId($nextStyleId); - - $this->serializedStyleToStyleIdMappingTable[$serializedStyle] = $nextStyleId; - $this->styleIdToStyleMappingTable[$nextStyleId] = $style; - } - - return $this->getStyleFromSerializedStyle($serializedStyle); - } - - /** - * Returns whether the given style has already been registered. - * - * @param \Box\Spout\Writer\Style\Style $style - * @return bool - */ - protected function hasStyleAlreadyBeenRegistered($style) - { - $serializedStyle = $style->serialize(); - - // Using isset here because it is way faster than array_key_exists... - return isset($this->serializedStyleToStyleIdMappingTable[$serializedStyle]); - } - - /** - * Returns the registered style associated to the given serialization. - * - * @param string $serializedStyle The serialized style from which the actual style should be fetched from - * @return \Box\Spout\Writer\Style\Style - */ - protected function getStyleFromSerializedStyle($serializedStyle) - { - $styleId = $this->serializedStyleToStyleIdMappingTable[$serializedStyle]; - return $this->styleIdToStyleMappingTable[$styleId]; - } - - /** - * @return \Box\Spout\Writer\Style\Style[] List of registered styles - */ - protected function getRegisteredStyles() - { - return array_values($this->styleIdToStyleMappingTable); - } - - /** - * Returns the default style - * - * @return \Box\Spout\Writer\Style\Style Default style - */ - protected function getDefaultStyle() - { - // By construction, the default style has ID 0 - return $this->styleIdToStyleMappingTable[0]; - } - - /** - * Apply additional styles if the given row needs it. - * Typically, set "wrap text" if a cell contains a new line. - * - * @param \Box\Spout\Writer\Style\Style $style The original style - * @param array $dataRow The row the style will be applied to - * @return \Box\Spout\Writer\Style\Style The updated style - */ - public function applyExtraStylesIfNeeded($style, $dataRow) - { - $updatedStyle = $this->applyWrapTextIfCellContainsNewLine($style, $dataRow); - return $updatedStyle; - } - - /** - * Set the "wrap text" option if a cell of the given row contains a new line. - * - * @NOTE: There is a bug on the Mac version of Excel (2011 and below) where new lines - * are ignored even when the "wrap text" option is set. This only occurs with - * inline strings (shared strings do work fine). - * A workaround would be to encode "\n" as "_x000D_" but it does not work - * on the Windows version of Excel... - * - * @param \Box\Spout\Writer\Style\Style $style The original style - * @param array $dataRow The row the style will be applied to - * @return \Box\Spout\Writer\Style\Style The eventually updated style - */ - protected function applyWrapTextIfCellContainsNewLine($style, $dataRow) - { - // if the "wrap text" option is already set, no-op - if ($style->hasSetWrapText()) { - return $style; - } - - foreach ($dataRow as $cell) { - if (is_string($cell) && strpos($cell, "\n") !== false) { - $style->setShouldWrapText(); - break; - } - } - - return $style; - } -} diff --git a/src/Spout/Writer/Common/Helper/CellHelper.php b/src/Spout/Writer/Common/Helper/CellHelper.php deleted file mode 100644 index 50ead93..0000000 --- a/src/Spout/Writer/Common/Helper/CellHelper.php +++ /dev/null @@ -1,91 +0,0 @@ - cell index */ - private static $columnIndexToCellIndexCache = []; - - /** - * Returns the cell index (base 26) associated to the base 10 column index. - * Excel uses A to Z letters for column indexing, where A is the 1st column, - * Z is the 26th and AA is the 27th. - * The mapping is zero based, so that 0 maps to A, B maps to 1, Z to 25 and AA to 26. - * - * @param int $columnIndex The Excel column index (0, 42, ...) - * @return string The associated cell index ('A', 'BC', ...) - */ - public static function getCellIndexFromColumnIndex($columnIndex) - { - $originalColumnIndex = $columnIndex; - - // Using isset here because it is way faster than array_key_exists... - if (!isset(self::$columnIndexToCellIndexCache[$originalColumnIndex])) { - $cellIndex = ''; - $capitalAAsciiValue = ord('A'); - - do { - $modulus = $columnIndex % 26; - $cellIndex = chr($capitalAAsciiValue + $modulus) . $cellIndex; - - // substracting 1 because it's zero-based - $columnIndex = intval($columnIndex / 26) - 1; - - } while ($columnIndex >= 0); - - self::$columnIndexToCellIndexCache[$originalColumnIndex] = $cellIndex; - } - - return self::$columnIndexToCellIndexCache[$originalColumnIndex]; - } - - /** - * @param $value - * @return bool Whether the given value is considered "empty" - */ - public static function isEmpty($value) - { - return ($value === null || $value === ''); - } - - /** - * @param $value - * @return bool Whether the given value is a non empty string - */ - public static function isNonEmptyString($value) - { - return (gettype($value) === 'string' && $value !== ''); - } - - /** - * Returns whether the given value is numeric. - * A numeric value is from type "integer" or "double" ("float" is not returned by gettype). - * - * @param $value - * @return bool Whether the given value is numeric - */ - public static function isNumeric($value) - { - $valueType = gettype($value); - return ($valueType === 'integer' || $valueType === 'double'); - } - - /** - * Returns whether the given value is boolean. - * "true"/"false" and 0/1 are not booleans. - * - * @param $value - * @return bool Whether the given value is boolean - */ - public static function isBoolean($value) - { - return gettype($value) === 'boolean'; - } -} diff --git a/src/Spout/Writer/Common/Helper/ZipHelper.php b/src/Spout/Writer/Common/Helper/ZipHelper.php deleted file mode 100644 index 1670d17..0000000 --- a/src/Spout/Writer/Common/Helper/ZipHelper.php +++ /dev/null @@ -1,217 +0,0 @@ -tmpFolderPath = $tmpFolderPath; - } - - /** - * Returns the already created ZipArchive instance or - * creates one if none exists. - * - * @return \ZipArchive - */ - protected function createOrGetZip() - { - if (!isset($this->zip)) { - $this->zip = new \ZipArchive(); - $zipFilePath = $this->getZipFilePath(); - - $this->zip->open($zipFilePath, \ZipArchive::CREATE|\ZipArchive::OVERWRITE); - } - - return $this->zip; - } - - /** - * @return string Path where the zip file of the given folder will be created - */ - public function getZipFilePath() - { - return $this->tmpFolderPath . self::ZIP_EXTENSION; - } - - /** - * Adds the given file, located under the given root folder to the archive. - * The file will be compressed. - * - * Example of use: - * addFileToArchive('/tmp/xlsx/foo', 'bar/baz.xml'); - * => will add the file located at '/tmp/xlsx/foo/bar/baz.xml' in the archive, but only as 'bar/baz.xml' - * - * @param string $rootFolderPath Path of the root folder that will be ignored in the archive tree. - * @param string $localFilePath Path of the file to be added, under the root folder - * @param string|void $existingFileMode Controls what to do when trying to add an existing file - * @return void - */ - public function addFileToArchive($rootFolderPath, $localFilePath, $existingFileMode = self::EXISTING_FILES_OVERWRITE) - { - $this->addFileToArchiveWithCompressionMethod( - $rootFolderPath, - $localFilePath, - $existingFileMode, - \ZipArchive::CM_DEFAULT - ); - } - - /** - * Adds the given file, located under the given root folder to the archive. - * The file will NOT be compressed. - * - * Example of use: - * addUncompressedFileToArchive('/tmp/xlsx/foo', 'bar/baz.xml'); - * => will add the file located at '/tmp/xlsx/foo/bar/baz.xml' in the archive, but only as 'bar/baz.xml' - * - * @param string $rootFolderPath Path of the root folder that will be ignored in the archive tree. - * @param string $localFilePath Path of the file to be added, under the root folder - * @param string|void $existingFileMode Controls what to do when trying to add an existing file - * @return void - */ - public function addUncompressedFileToArchive($rootFolderPath, $localFilePath, $existingFileMode = self::EXISTING_FILES_OVERWRITE) - { - $this->addFileToArchiveWithCompressionMethod( - $rootFolderPath, - $localFilePath, - $existingFileMode, - \ZipArchive::CM_STORE - ); - } - - /** - * Adds the given file, located under the given root folder to the archive. - * The file will NOT be compressed. - * - * Example of use: - * addUncompressedFileToArchive('/tmp/xlsx/foo', 'bar/baz.xml'); - * => will add the file located at '/tmp/xlsx/foo/bar/baz.xml' in the archive, but only as 'bar/baz.xml' - * - * @param string $rootFolderPath Path of the root folder that will be ignored in the archive tree. - * @param string $localFilePath Path of the file to be added, under the root folder - * @param string $existingFileMode Controls what to do when trying to add an existing file - * @param int $compressionMethod The compression method - * @return void - */ - protected function addFileToArchiveWithCompressionMethod($rootFolderPath, $localFilePath, $existingFileMode, $compressionMethod) - { - $zip = $this->createOrGetZip(); - - if (!$this->shouldSkipFile($zip, $localFilePath, $existingFileMode)) { - $normalizedFullFilePath = $this->getNormalizedRealPath($rootFolderPath . '/' . $localFilePath); - $zip->addFile($normalizedFullFilePath, $localFilePath); - - if (self::canChooseCompressionMethod()) { - $zip->setCompressionName($localFilePath, $compressionMethod); - } - } - } - - /** - * @return bool Whether it is possible to choose the desired compression method to be used - */ - public static function canChooseCompressionMethod() - { - // setCompressionName() is a PHP7+ method... - return (method_exists(new \ZipArchive(), 'setCompressionName')); - } - - /** - * @param string $folderPath Path to the folder to be zipped - * @param string|void $existingFileMode Controls what to do when trying to add an existing file - * @return void - */ - public function addFolderToArchive($folderPath, $existingFileMode = self::EXISTING_FILES_OVERWRITE) - { - $zip = $this->createOrGetZip(); - - $folderRealPath = $this->getNormalizedRealPath($folderPath) . '/'; - $itemIterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($folderPath, \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::SELF_FIRST); - - foreach ($itemIterator as $itemInfo) { - $itemRealPath = $this->getNormalizedRealPath($itemInfo->getPathname()); - $itemLocalPath = str_replace($folderRealPath, '', $itemRealPath); - - if ($itemInfo->isFile() && !$this->shouldSkipFile($zip, $itemLocalPath, $existingFileMode)) { - $zip->addFile($itemRealPath, $itemLocalPath); - } - } - } - - /** - * @param \ZipArchive $zip - * @param string $itemLocalPath - * @param string $existingFileMode - * @return bool Whether the file should be added to the archive or skipped - */ - protected function shouldSkipFile($zip, $itemLocalPath, $existingFileMode) - { - // Skip files if: - // - EXISTING_FILES_SKIP mode chosen - // - File already exists in the archive - return ($existingFileMode === self::EXISTING_FILES_SKIP && $zip->locateName($itemLocalPath) !== false); - } - - /** - * Returns canonicalized absolute pathname, containing only forward slashes. - * - * @param string $path Path to normalize - * @return string Normalized and canonicalized path - */ - protected function getNormalizedRealPath($path) - { - $realPath = realpath($path); - return str_replace(DIRECTORY_SEPARATOR, '/', $realPath); - } - - /** - * Closes the archive and copies it into the given stream - * - * @param resource $streamPointer Pointer to the stream to copy the zip - * @return void - */ - public function closeArchiveAndCopyToStream($streamPointer) - { - $zip = $this->createOrGetZip(); - $zip->close(); - unset($this->zip); - - $this->copyZipToStream($streamPointer); - } - - /** - * Streams the contents of the zip file into the given stream - * - * @param resource $pointer Pointer to the stream to copy the zip - * @return void - */ - protected function copyZipToStream($pointer) - { - $zipFilePointer = fopen($this->getZipFilePath(), 'r'); - stream_copy_to_stream($zipFilePointer, $pointer); - fclose($zipFilePointer); - } -} diff --git a/src/Spout/Writer/Common/Internal/AbstractWorkbook.php b/src/Spout/Writer/Common/Internal/AbstractWorkbook.php deleted file mode 100644 index e852e1a..0000000 --- a/src/Spout/Writer/Common/Internal/AbstractWorkbook.php +++ /dev/null @@ -1,192 +0,0 @@ -shouldCreateNewSheetsAutomatically = $shouldCreateNewSheetsAutomatically; - $this->internalId = uniqid(); - } - - /** - * @return \Box\Spout\Writer\Common\Helper\AbstractStyleHelper The specific style helper - */ - abstract protected function getStyleHelper(); - - /** - * @return int Maximum number of rows/columns a sheet can contain - */ - abstract protected function getMaxRowsPerWorksheet(); - - /** - * Creates a new sheet in the workbook. The current sheet remains unchanged. - * - * @return WorksheetInterface The created sheet - * @throws \Box\Spout\Common\Exception\IOException If unable to open the sheet for writing - */ - abstract public function addNewSheet(); - - /** - * Creates a new sheet in the workbook and make it the current sheet. - * The writing will resume where it stopped (i.e. data won't be truncated). - * - * @return WorksheetInterface The created sheet - * @throws \Box\Spout\Common\Exception\IOException If unable to open the sheet for writing - */ - public function addNewSheetAndMakeItCurrent() - { - $worksheet = $this->addNewSheet(); - $this->setCurrentWorksheet($worksheet); - - return $worksheet; - } - - /** - * @return WorksheetInterface[] All the workbook's sheets - */ - public function getWorksheets() - { - return $this->worksheets; - } - - /** - * Returns the current sheet - * - * @return WorksheetInterface The current sheet - */ - public function getCurrentWorksheet() - { - return $this->currentWorksheet; - } - - /** - * Sets the given sheet as the current one. New data will be written to this sheet. - * The writing will resume where it stopped (i.e. data won't be truncated). - * - * @param \Box\Spout\Writer\Common\Sheet $sheet The "external" sheet to set as current - * @return void - * @throws \Box\Spout\Writer\Exception\SheetNotFoundException If the given sheet does not exist in the workbook - */ - public function setCurrentSheet($sheet) - { - $worksheet = $this->getWorksheetFromExternalSheet($sheet); - if ($worksheet !== null) { - $this->currentWorksheet = $worksheet; - } else { - throw new SheetNotFoundException('The given sheet does not exist in the workbook.'); - } - } - - /** - * @param WorksheetInterface $worksheet - * @return void - */ - protected function setCurrentWorksheet($worksheet) - { - $this->currentWorksheet = $worksheet; - } - - /** - * Returns the worksheet associated to the given external sheet. - * - * @param \Box\Spout\Writer\Common\Sheet $sheet - * @return WorksheetInterface|null The worksheet associated to the given external sheet or null if not found. - */ - protected function getWorksheetFromExternalSheet($sheet) - { - $worksheetFound = null; - - foreach ($this->worksheets as $worksheet) { - if ($worksheet->getExternalSheet() === $sheet) { - $worksheetFound = $worksheet; - break; - } - } - - return $worksheetFound; - } - - /** - * Adds data to the current sheet. - * If shouldCreateNewSheetsAutomatically option is set to true, it will handle pagination - * with the creation of new worksheets if one worksheet has reached its maximum capicity. - * - * @param array $dataRow Array containing data to be written. Cannot be empty. - * Example $dataRow = ['data1', 1234, null, '', 'data5']; - * @param \Box\Spout\Writer\Style\Style $style Style to be applied to the row. - * @return void - * @throws \Box\Spout\Common\Exception\IOException If trying to create a new sheet and unable to open the sheet for writing - * @throws \Box\Spout\Writer\Exception\WriterException If unable to write data - */ - public function addRowToCurrentWorksheet($dataRow, $style) - { - $currentWorksheet = $this->getCurrentWorksheet(); - $hasReachedMaxRows = $this->hasCurrentWorkseetReachedMaxRows(); - $styleHelper = $this->getStyleHelper(); - - // if we reached the maximum number of rows for the current sheet... - if ($hasReachedMaxRows) { - // ... continue writing in a new sheet if option set - if ($this->shouldCreateNewSheetsAutomatically) { - $currentWorksheet = $this->addNewSheetAndMakeItCurrent(); - - $updatedStyle = $styleHelper->applyExtraStylesIfNeeded($style, $dataRow); - $registeredStyle = $styleHelper->registerStyle($updatedStyle); - $currentWorksheet->addRow($dataRow, $registeredStyle); - } else { - // otherwise, do nothing as the data won't be read anyways - } - } else { - $updatedStyle = $styleHelper->applyExtraStylesIfNeeded($style, $dataRow); - $registeredStyle = $styleHelper->registerStyle($updatedStyle); - $currentWorksheet->addRow($dataRow, $registeredStyle); - } - } - - /** - * @return bool Whether the current worksheet has reached the maximum number of rows per sheet. - */ - protected function hasCurrentWorkseetReachedMaxRows() - { - $currentWorksheet = $this->getCurrentWorksheet(); - return ($currentWorksheet->getLastWrittenRowIndex() >= $this->getMaxRowsPerWorksheet()); - } - - /** - * Closes the workbook and all its associated sheets. - * All the necessary files are written to disk and zipped together to create the ODS file. - * All the temporary files are then deleted. - * - * @param resource $finalFilePointer Pointer to the ODS that will be created - * @return void - */ - abstract public function close($finalFilePointer); -} diff --git a/src/Spout/Writer/Common/Internal/WorkbookInterface.php b/src/Spout/Writer/Common/Internal/WorkbookInterface.php deleted file mode 100644 index fda0793..0000000 --- a/src/Spout/Writer/Common/Internal/WorkbookInterface.php +++ /dev/null @@ -1,74 +0,0 @@ - [[SHEET_INDEX] => [SHEET_NAME]] keeping track of sheets' name to enforce uniqueness per workbook */ - protected static $SHEETS_NAME_USED = []; - - /** @var int Index of the sheet, based on order in the workbook (zero-based) */ - protected $index; - - /** @var string ID of the sheet's associated workbook. Used to restrict sheet name uniqueness enforcement to a single workbook */ - protected $associatedWorkbookId; - - /** @var string Name of the sheet */ - protected $name; - - /** @var \Box\Spout\Common\Helper\StringHelper */ - protected $stringHelper; - - /** - * @param int $sheetIndex Index of the sheet, based on order in the workbook (zero-based) - * @param string $associatedWorkbookId ID of the sheet's associated workbook - */ - public function __construct($sheetIndex, $associatedWorkbookId) - { - $this->index = $sheetIndex; - $this->associatedWorkbookId = $associatedWorkbookId; - if (!isset(self::$SHEETS_NAME_USED[$associatedWorkbookId])) { - self::$SHEETS_NAME_USED[$associatedWorkbookId] = []; - } - - $this->stringHelper = new StringHelper(); - $this->setName(self::DEFAULT_SHEET_NAME_PREFIX . ($sheetIndex + 1)); - } - - /** - * @api - * @return int Index of the sheet, based on order in the workbook (zero-based) - */ - public function getIndex() - { - return $this->index; - } - - /** - * @api - * @return string Name of the sheet - */ - public function getName() - { - return $this->name; - } - - /** - * Sets the name of the sheet. Note that Excel has some restrictions on the name: - * - it should not be blank - * - it should not exceed 31 characters - * - it should not contain these characters: \ / ? * : [ or ] - * - it should be unique - * - * @api - * @param string $name Name of the sheet - * @return Sheet - * @throws \Box\Spout\Writer\Exception\InvalidSheetNameException If the sheet's name is invalid. - */ - public function setName($name) - { - $this->throwIfNameIsInvalid($name); - - $this->name = $name; - self::$SHEETS_NAME_USED[$this->associatedWorkbookId][$this->index] = $name; - - return $this; - } - - /** - * Throws an exception if the given sheet's name is not valid. - * @see Sheet::setName for validity rules. - * - * @param string $name - * @return void - * @throws \Box\Spout\Writer\Exception\InvalidSheetNameException If the sheet's name is invalid. - */ - protected function throwIfNameIsInvalid($name) - { - if (!is_string($name)) { - $actualType = gettype($name); - $errorMessage = "The sheet's name is invalid. It must be a string ($actualType given)."; - throw new InvalidSheetNameException($errorMessage); - } - - $failedRequirements = []; - $nameLength = $this->stringHelper->getStringLength($name); - - if (!$this->isNameUnique($name)) { - $failedRequirements[] = 'It should be unique'; - } else { - if ($nameLength === 0) { - $failedRequirements[] = 'It should not be blank'; - } else { - if ($nameLength > self::MAX_LENGTH_SHEET_NAME) { - $failedRequirements[] = 'It should not exceed 31 characters'; - } - - if ($this->doesContainInvalidCharacters($name)) { - $failedRequirements[] = 'It should not contain these characters: \\ / ? * : [ or ]'; - } - - if ($this->doesStartOrEndWithSingleQuote($name)) { - $failedRequirements[] = 'It should not start or end with a single quote'; - } - } - } - - if (count($failedRequirements) !== 0) { - $errorMessage = "The sheet's name (\"$name\") is invalid. It did not respect these rules:\n - "; - $errorMessage .= implode("\n - ", $failedRequirements); - throw new InvalidSheetNameException($errorMessage); - } - } - - /** - * Returns whether the given name contains at least one invalid character. - * @see Sheet::$INVALID_CHARACTERS_IN_SHEET_NAME for the full list. - * - * @param string $name - * @return bool TRUE if the name contains invalid characters, FALSE otherwise. - */ - protected function doesContainInvalidCharacters($name) - { - return (str_replace(self::$INVALID_CHARACTERS_IN_SHEET_NAME, '', $name) !== $name); - } - - /** - * Returns whether the given name starts or ends with a single quote - * - * @param string $name - * @return bool TRUE if the name starts or ends with a single quote, FALSE otherwise. - */ - protected function doesStartOrEndWithSingleQuote($name) - { - $startsWithSingleQuote = ($this->stringHelper->getCharFirstOccurrencePosition('\'', $name) === 0); - $endsWithSingleQuote = ($this->stringHelper->getCharLastOccurrencePosition('\'', $name) === ($this->stringHelper->getStringLength($name) - 1)); - - return ($startsWithSingleQuote || $endsWithSingleQuote); - } - - /** - * Returns whether the given name is unique. - * - * @param string $name - * @return bool TRUE if the name is unique, FALSE otherwise. - */ - protected function isNameUnique($name) - { - foreach (self::$SHEETS_NAME_USED[$this->associatedWorkbookId] as $sheetIndex => $sheetName) { - if ($sheetIndex !== $this->index && $sheetName === $name) { - return false; - } - } - - return true; - } -} diff --git a/src/Spout/Writer/Exception/Border/InvalidNameException.php b/src/Spout/Writer/Exception/Border/InvalidNameException.php deleted file mode 100644 index 13ac06c..0000000 --- a/src/Spout/Writer/Exception/Border/InvalidNameException.php +++ /dev/null @@ -1,16 +0,0 @@ - - */ -class BorderHelper -{ - /** - * Width mappings - * - * @var array - */ - protected static $widthMap = [ - Border::WIDTH_THIN => '0.75pt', - Border::WIDTH_MEDIUM => '1.75pt', - Border::WIDTH_THICK => '2.5pt', - ]; - - /** - * Style mapping - * - * @var array - */ - protected static $styleMap = [ - Border::STYLE_SOLID => 'solid', - Border::STYLE_DASHED => 'dashed', - Border::STYLE_DOTTED => 'dotted', - Border::STYLE_DOUBLE => 'double', - ]; - - /** - * @param BorderPart $borderPart - * @return string - */ - public static function serializeBorderPart(BorderPart $borderPart) - { - $definition = 'fo:border-%s="%s"'; - - if ($borderPart->getStyle() === Border::STYLE_NONE) { - $borderPartDefinition = sprintf($definition, $borderPart->getName(), 'none'); - } else { - $attributes = [ - self::$widthMap[$borderPart->getWidth()], - self::$styleMap[$borderPart->getStyle()], - '#' . $borderPart->getColor(), - ]; - $borderPartDefinition = sprintf($definition, $borderPart->getName(), implode(' ', $attributes)); - } - - return $borderPartDefinition; - } -} diff --git a/src/Spout/Writer/ODS/Helper/FileSystemHelper.php b/src/Spout/Writer/ODS/Helper/FileSystemHelper.php deleted file mode 100644 index 34ace0a..0000000 --- a/src/Spout/Writer/ODS/Helper/FileSystemHelper.php +++ /dev/null @@ -1,279 +0,0 @@ -rootFolder; - } - - /** - * @return string - */ - public function getSheetsContentTempFolder() - { - return $this->sheetsContentTempFolder; - } - - /** - * Creates all the folders needed to create a ODS file, as well as the files that won't change. - * - * @return void - * @throws \Box\Spout\Common\Exception\IOException If unable to create at least one of the base folders - */ - public function createBaseFilesAndFolders() - { - $this - ->createRootFolder() - ->createMetaInfoFolderAndFile() - ->createSheetsContentTempFolder() - ->createMetaFile() - ->createMimetypeFile(); - } - - /** - * Creates the folder that will be used as root - * - * @return FileSystemHelper - * @throws \Box\Spout\Common\Exception\IOException If unable to create the folder - */ - protected function createRootFolder() - { - $this->rootFolder = $this->createFolder($this->baseFolderRealPath, uniqid('ods')); - return $this; - } - - /** - * Creates the "META-INF" folder under the root folder as well as the "manifest.xml" file in it - * - * @return FileSystemHelper - * @throws \Box\Spout\Common\Exception\IOException If unable to create the folder or the "manifest.xml" file - */ - protected function createMetaInfoFolderAndFile() - { - $this->metaInfFolder = $this->createFolder($this->rootFolder, self::META_INF_FOLDER_NAME); - - $this->createManifestFile(); - - return $this; - } - - /** - * Creates the "manifest.xml" file under the "META-INF" folder (under root) - * - * @return FileSystemHelper - * @throws \Box\Spout\Common\Exception\IOException If unable to create the file - */ - protected function createManifestFile() - { - $manifestXmlFileContents = << - - - - - - -EOD; - - $this->createFileWithContents($this->metaInfFolder, self::MANIFEST_XML_FILE_NAME, $manifestXmlFileContents); - - return $this; - } - - /** - * Creates the temp folder where specific sheets content will be written to. - * This folder is not part of the final ODS file and is only used to be able to jump between sheets. - * - * @return FileSystemHelper - * @throws \Box\Spout\Common\Exception\IOException If unable to create the folder - */ - protected function createSheetsContentTempFolder() - { - $this->sheetsContentTempFolder = $this->createFolder($this->rootFolder, self::SHEETS_CONTENT_TEMP_FOLDER_NAME); - return $this; - } - - /** - * Creates the "meta.xml" file under the root folder - * - * @return FileSystemHelper - * @throws \Box\Spout\Common\Exception\IOException If unable to create the file - */ - protected function createMetaFile() - { - $appName = self::APP_NAME; - $createdDate = (new \DateTime())->format(\DateTime::W3C); - - $metaXmlFileContents = << - - - $appName - $createdDate - $createdDate - - -EOD; - - $this->createFileWithContents($this->rootFolder, self::META_XML_FILE_NAME, $metaXmlFileContents); - - return $this; - } - - /** - * Creates the "mimetype" file under the root folder - * - * @return FileSystemHelper - * @throws \Box\Spout\Common\Exception\IOException If unable to create the file - */ - protected function createMimetypeFile() - { - $this->createFileWithContents($this->rootFolder, self::MIMETYPE_FILE_NAME, self::MIMETYPE); - return $this; - } - - /** - * Creates the "content.xml" file under the root folder - * - * @param Worksheet[] $worksheets - * @param StyleHelper $styleHelper - * @return FileSystemHelper - */ - public function createContentFile($worksheets, $styleHelper) - { - $contentXmlFileContents = << - -EOD; - - $contentXmlFileContents .= $styleHelper->getContentXmlFontFaceSectionContent(); - $contentXmlFileContents .= $styleHelper->getContentXmlAutomaticStylesSectionContent(count($worksheets)); - - $contentXmlFileContents .= ''; - - $this->createFileWithContents($this->rootFolder, self::CONTENT_XML_FILE_NAME, $contentXmlFileContents); - - // Append sheets content to "content.xml" - $contentXmlFilePath = $this->rootFolder . '/' . self::CONTENT_XML_FILE_NAME; - $contentXmlHandle = fopen($contentXmlFilePath, 'a'); - - foreach ($worksheets as $worksheet) { - // write the "" node, with the final sheet's name - fwrite($contentXmlHandle, $worksheet->getTableElementStartAsString()); - - $worksheetFilePath = $worksheet->getWorksheetFilePath(); - $this->copyFileContentsToTarget($worksheetFilePath, $contentXmlHandle); - - fwrite($contentXmlHandle, ''); - } - - $contentXmlFileContents = ''; - - fwrite($contentXmlHandle, $contentXmlFileContents); - fclose($contentXmlHandle); - - return $this; - } - - /** - * Streams the content of the file at the given path into the target resource. - * Depending on which mode the target resource was created with, it will truncate then copy - * or append the content to the target file. - * - * @param string $sourceFilePath Path of the file whose content will be copied - * @param resource $targetResource Target resource that will receive the content - * @return void - */ - protected function copyFileContentsToTarget($sourceFilePath, $targetResource) - { - $sourceHandle = fopen($sourceFilePath, 'r'); - stream_copy_to_stream($sourceHandle, $targetResource); - fclose($sourceHandle); - } - - /** - * Deletes the temporary folder where sheets content was stored. - * - * @return FileSystemHelper - */ - public function deleteWorksheetTempFolder() - { - $this->deleteFolderRecursively($this->sheetsContentTempFolder); - return $this; - } - - - /** - * Creates the "styles.xml" file under the root folder - * - * @param StyleHelper $styleHelper - * @param int $numWorksheets Number of created worksheets - * @return FileSystemHelper - */ - public function createStylesFile($styleHelper, $numWorksheets) - { - $stylesXmlFileContents = $styleHelper->getStylesXMLFileContent($numWorksheets); - $this->createFileWithContents($this->rootFolder, self::STYLES_XML_FILE_NAME, $stylesXmlFileContents); - - return $this; - } - - /** - * Zips the root folder and streams the contents of the zip into the given stream - * - * @param resource $streamPointer Pointer to the stream to copy the zip - * @return void - */ - public function zipRootFolderAndCopyToStream($streamPointer) - { - $zipHelper = new ZipHelper($this->rootFolder); - - // In order to have the file's mime type detected properly, files need to be added - // to the zip file in a particular order. - // @see http://www.jejik.com/articles/2010/03/how_to_correctly_create_odf_documents_using_zip/ - $zipHelper->addUncompressedFileToArchive($this->rootFolder, self::MIMETYPE_FILE_NAME); - - $zipHelper->addFolderToArchive($this->rootFolder, ZipHelper::EXISTING_FILES_SKIP); - $zipHelper->closeArchiveAndCopyToStream($streamPointer); - - // once the zip is copied, remove it - $this->deleteFile($zipHelper->getZipFilePath()); - } -} diff --git a/src/Spout/Writer/ODS/Helper/StyleHelper.php b/src/Spout/Writer/ODS/Helper/StyleHelper.php deleted file mode 100644 index f5ad3bc..0000000 --- a/src/Spout/Writer/ODS/Helper/StyleHelper.php +++ /dev/null @@ -1,356 +0,0 @@ - [] Map whose keys contain all the fonts used */ - protected $usedFontsSet = []; - - /** - * Registers the given style as a used style. - * Duplicate styles won't be registered more than once. - * - * @param \Box\Spout\Writer\Style\Style $style The style to be registered - * @return \Box\Spout\Writer\Style\Style The registered style, updated with an internal ID. - */ - public function registerStyle($style) - { - $this->usedFontsSet[$style->getFontName()] = true; - return parent::registerStyle($style); - } - - /** - * @return string[] List of used fonts name - */ - protected function getUsedFonts() - { - return array_keys($this->usedFontsSet); - } - - /** - * Returns the content of the "styles.xml" file, given a list of styles. - * - * @param int $numWorksheets Number of worksheets created - * @return string - */ - public function getStylesXMLFileContent($numWorksheets) - { - $content = << - -EOD; - - $content .= $this->getFontFaceSectionContent(); - $content .= $this->getStylesSectionContent(); - $content .= $this->getAutomaticStylesSectionContent($numWorksheets); - $content .= $this->getMasterStylesSectionContent($numWorksheets); - - $content .= << -EOD; - - return $content; - } - - /** - * Returns the content of the "" section, inside "styles.xml" file. - * - * @return string - */ - protected function getFontFaceSectionContent() - { - $content = ''; - foreach ($this->getUsedFonts() as $fontName) { - $content .= ''; - } - $content .= ''; - - return $content; - } - - /** - * Returns the content of the "" section, inside "styles.xml" file. - * - * @return string - */ - protected function getStylesSectionContent() - { - $defaultStyle = $this->getDefaultStyle(); - - return << - - - - - - - - -EOD; - } - - /** - * Returns the content of the "" section, inside "styles.xml" file. - * - * @param int $numWorksheets Number of worksheets created - * @return string - */ - protected function getAutomaticStylesSectionContent($numWorksheets) - { - $content = ''; - - for ($i = 1; $i <= $numWorksheets; $i++) { - $content .= << - - - - -EOD; - } - - $content .= ''; - - return $content; - } - - /** - * Returns the content of the "" section, inside "styles.xml" file. - * - * @param int $numWorksheets Number of worksheets created - * @return string - */ - protected function getMasterStylesSectionContent($numWorksheets) - { - $content = ''; - - for ($i = 1; $i <= $numWorksheets; $i++) { - $content .= << - - - - - -EOD; - } - - $content .= ''; - - return $content; - } - - - /** - * Returns the contents of the "" section, inside "content.xml" file. - * - * @return string - */ - public function getContentXmlFontFaceSectionContent() - { - $content = ''; - foreach ($this->getUsedFonts() as $fontName) { - $content .= ''; - } - $content .= ''; - - return $content; - } - - /** - * Returns the contents of the "" section, inside "content.xml" file. - * - * @param int $numWorksheets Number of worksheets created - * @return string - */ - public function getContentXmlAutomaticStylesSectionContent($numWorksheets) - { - $content = ''; - - foreach ($this->getRegisteredStyles() as $style) { - $content .= $this->getStyleSectionContent($style); - } - - $content .= << - - - - - -EOD; - - for ($i = 1; $i <= $numWorksheets; $i++) { - $content .= << - - -EOD; - } - - $content .= ''; - - return $content; - } - - /** - * Returns the contents of the "" section, inside "" section - * - * @param \Box\Spout\Writer\Style\Style $style - * @return string - */ - protected function getStyleSectionContent($style) - { - $styleIndex = $style->getId() + 1; // 1-based - - $content = ''; - - $content .= $this->getTextPropertiesSectionContent($style); - $content .= $this->getTableCellPropertiesSectionContent($style); - - $content .= ''; - - return $content; - } - - /** - * Returns the contents of the "" section, inside "" section - * - * @param \Box\Spout\Writer\Style\Style $style - * @return string - */ - private function getTextPropertiesSectionContent($style) - { - $content = ''; - - if ($style->shouldApplyFont()) { - $content .= $this->getFontSectionContent($style); - } - - return $content; - } - - /** - * Returns the contents of the "" section, inside "" section - * - * @param \Box\Spout\Writer\Style\Style $style - * @return string - */ - private function getFontSectionContent($style) - { - $defaultStyle = $this->getDefaultStyle(); - - $content = 'getFontColor(); - if ($fontColor !== $defaultStyle->getFontColor()) { - $content .= ' fo:color="#' . $fontColor . '"'; - } - - $fontName = $style->getFontName(); - if ($fontName !== $defaultStyle->getFontName()) { - $content .= ' style:font-name="' . $fontName . '" style:font-name-asian="' . $fontName . '" style:font-name-complex="' . $fontName . '"'; - } - - $fontSize = $style->getFontSize(); - if ($fontSize !== $defaultStyle->getFontSize()) { - $content .= ' fo:font-size="' . $fontSize . 'pt" style:font-size-asian="' . $fontSize . 'pt" style:font-size-complex="' . $fontSize . 'pt"'; - } - - if ($style->isFontBold()) { - $content .= ' fo:font-weight="bold" style:font-weight-asian="bold" style:font-weight-complex="bold"'; - } - if ($style->isFontItalic()) { - $content .= ' fo:font-style="italic" style:font-style-asian="italic" style:font-style-complex="italic"'; - } - if ($style->isFontUnderline()) { - $content .= ' style:text-underline-style="solid" style:text-underline-type="single"'; - } - if ($style->isFontStrikethrough()) { - $content .= ' style:text-line-through-style="solid"'; - } - - $content .= '/>'; - - return $content; - } - - /** - * Returns the contents of the "" section, inside "" section - * - * @param \Box\Spout\Writer\Style\Style $style - * @return string - */ - private function getTableCellPropertiesSectionContent($style) - { - $content = ''; - - if ($style->shouldWrapText()) { - $content .= $this->getWrapTextXMLContent(); - } - - if ($style->shouldApplyBorder()) { - $content .= $this->getBorderXMLContent($style); - } - - if ($style->shouldApplyBackgroundColor()) { - $content .= $this->getBackgroundColorXMLContent($style); - } - - return $content; - } - - /** - * Returns the contents of the wrap text definition for the "" section - * - * @return string - */ - private function getWrapTextXMLContent() - { - return ''; - } - - /** - * Returns the contents of the borders definition for the "" section - * - * @param \Box\Spout\Writer\Style\Style $style - * @return string - */ - private function getBorderXMLContent($style) - { - $borderProperty = ''; - - $borders = array_map(function (BorderPart $borderPart) { - return BorderHelper::serializeBorderPart($borderPart); - }, $style->getBorder()->getParts()); - - return sprintf($borderProperty, implode(' ', $borders)); - } - - /** - * Returns the contents of the background color definition for the "" section - * - * @param \Box\Spout\Writer\Style\Style $style - * @return string - */ - private function getBackgroundColorXMLContent($style) - { - return sprintf( - '', - $style->getBackgroundColor() - ); - } -} diff --git a/src/Spout/Writer/ODS/Internal/Workbook.php b/src/Spout/Writer/ODS/Internal/Workbook.php deleted file mode 100644 index fc64ada..0000000 --- a/src/Spout/Writer/ODS/Internal/Workbook.php +++ /dev/null @@ -1,119 +0,0 @@ -fileSystemHelper = new FileSystemHelper($tempFolder); - $this->fileSystemHelper->createBaseFilesAndFolders(); - - $this->styleHelper = new StyleHelper($defaultRowStyle); - } - - /** - * @return \Box\Spout\Writer\ODS\Helper\StyleHelper Helper to apply styles to ODS files - */ - protected function getStyleHelper() - { - return $this->styleHelper; - } - - /** - * @return int Maximum number of rows/columns a sheet can contain - */ - protected function getMaxRowsPerWorksheet() - { - return self::$maxRowsPerWorksheet; - } - - /** - * Creates a new sheet in the workbook. The current sheet remains unchanged. - * - * @return Worksheet The created sheet - * @throws \Box\Spout\Common\Exception\IOException If unable to open the sheet for writing - */ - public function addNewSheet() - { - $newSheetIndex = count($this->worksheets); - $sheet = new Sheet($newSheetIndex, $this->internalId); - - $sheetsContentTempFolder = $this->fileSystemHelper->getSheetsContentTempFolder(); - $worksheet = new Worksheet($sheet, $sheetsContentTempFolder); - $this->worksheets[] = $worksheet; - - return $worksheet; - } - - /** - * Closes the workbook and all its associated sheets. - * All the necessary files are written to disk and zipped together to create the ODS file. - * All the temporary files are then deleted. - * - * @param resource $finalFilePointer Pointer to the ODS that will be created - * @return void - */ - public function close($finalFilePointer) - { - /** @var Worksheet[] $worksheets */ - $worksheets = $this->worksheets; - $numWorksheets = count($worksheets); - - foreach ($worksheets as $worksheet) { - $worksheet->close(); - } - - // Finish creating all the necessary files before zipping everything together - $this->fileSystemHelper - ->createContentFile($worksheets, $this->styleHelper) - ->deleteWorksheetTempFolder() - ->createStylesFile($this->styleHelper, $numWorksheets) - ->zipRootFolderAndCopyToStream($finalFilePointer); - - $this->cleanupTempFolder(); - } - - /** - * Deletes the root folder created in the temp folder and all its contents. - * - * @return void - */ - protected function cleanupTempFolder() - { - $xlsxRootFolder = $this->fileSystemHelper->getRootFolder(); - $this->fileSystemHelper->deleteFolderRecursively($xlsxRootFolder); - } -} diff --git a/src/Spout/Writer/ODS/Internal/Worksheet.php b/src/Spout/Writer/ODS/Internal/Worksheet.php deleted file mode 100644 index 0920b6d..0000000 --- a/src/Spout/Writer/ODS/Internal/Worksheet.php +++ /dev/null @@ -1,233 +0,0 @@ -externalSheet = $externalSheet; - /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ - $this->stringsEscaper = \Box\Spout\Common\Escaper\ODS::getInstance(); - $this->worksheetFilePath = $worksheetFilesFolder . '/sheet' . $externalSheet->getIndex() . '.xml'; - - $this->stringHelper = new StringHelper(); - - $this->startSheet(); - } - - /** - * Prepares the worksheet to accept data - * The XML file does not contain the "" node as it contains the sheet's name - * which may change during the execution of the program. It will be added at the end. - * - * @return void - * @throws \Box\Spout\Common\Exception\IOException If the sheet data file cannot be opened for writing - */ - protected function startSheet() - { - $this->sheetFilePointer = fopen($this->worksheetFilePath, 'w'); - $this->throwIfSheetFilePointerIsNotAvailable(); - } - - /** - * Checks if the book has been created. Throws an exception if not created yet. - * - * @return void - * @throws \Box\Spout\Common\Exception\IOException If the sheet data file cannot be opened for writing - */ - protected function throwIfSheetFilePointerIsNotAvailable() - { - if (!$this->sheetFilePointer) { - throw new IOException('Unable to open sheet for writing.'); - } - } - - /** - * @return string Path to the temporary sheet content XML file - */ - public function getWorksheetFilePath() - { - return $this->worksheetFilePath; - } - - /** - * Returns the table XML root node as string. - * - * @return string node as string - */ - public function getTableElementStartAsString() - { - $escapedSheetName = $this->stringsEscaper->escape($this->externalSheet->getName()); - $tableStyleName = 'ta' . ($this->externalSheet->getIndex() + 1); - - $tableElement = ''; - $tableElement .= ''; - - return $tableElement; - } - - /** - * @return \Box\Spout\Writer\Common\Sheet The "external" sheet - */ - public function getExternalSheet() - { - return $this->externalSheet; - } - - /** - * @return int The index of the last written row - */ - public function getLastWrittenRowIndex() - { - return $this->lastWrittenRowIndex; - } - - /** - * Adds data to the worksheet. - * - * @param array $dataRow Array containing data to be written. Cannot be empty. - * Example $dataRow = ['data1', 1234, null, '', 'data5']; - * @param \Box\Spout\Writer\Style\Style $style Style to be applied to the row. NULL means use default style. - * @return void - * @throws \Box\Spout\Common\Exception\IOException If the data cannot be written - * @throws \Box\Spout\Common\Exception\InvalidArgumentException If a cell value's type is not supported - */ - public function addRow($dataRow, $style) - { - // $dataRow can be an associative array. We need to transform - // it into a regular array, as we'll use the numeric indexes. - $dataRowWithNumericIndexes = array_values($dataRow); - - $styleIndex = ($style->getId() + 1); // 1-based - $cellsCount = count($dataRow); - $this->maxNumColumns = max($this->maxNumColumns, $cellsCount); - - $data = ''; - - $currentCellIndex = 0; - $nextCellIndex = 1; - - for ($i = 0; $i < $cellsCount; $i++) { - $currentCellValue = $dataRowWithNumericIndexes[$currentCellIndex]; - - // Using isset here because it is way faster than array_key_exists... - if (!isset($dataRowWithNumericIndexes[$nextCellIndex]) || - $currentCellValue !== $dataRowWithNumericIndexes[$nextCellIndex]) { - - $numTimesValueRepeated = ($nextCellIndex - $currentCellIndex); - $data .= $this->getCellXML($currentCellValue, $styleIndex, $numTimesValueRepeated); - - $currentCellIndex = $nextCellIndex; - } - - $nextCellIndex++; - } - - $data .= ''; - - $wasWriteSuccessful = fwrite($this->sheetFilePointer, $data); - if ($wasWriteSuccessful === false) { - throw new IOException("Unable to write data in {$this->worksheetFilePath}"); - } - - // only update the count if the write worked - $this->lastWrittenRowIndex++; - } - - /** - * Returns the cell XML content, given its value. - * - * @param mixed $cellValue The value to be written - * @param int $styleIndex Index of the used style - * @param int $numTimesValueRepeated Number of times the value is consecutively repeated - * @return string The cell XML content - * @throws \Box\Spout\Common\Exception\InvalidArgumentException If a cell value's type is not supported - */ - protected function getCellXML($cellValue, $styleIndex, $numTimesValueRepeated) - { - $data = 'stringsEscaper->escape($cellValueLine) . ''; - } - - $data .= ''; - } else if (CellHelper::isBoolean($cellValue)) { - $data .= ' office:value-type="boolean" calcext:value-type="boolean" office:boolean-value="' . $cellValue . '">'; - $data .= '' . $cellValue . ''; - $data .= ''; - } else if (CellHelper::isNumeric($cellValue)) { - $data .= ' office:value-type="float" calcext:value-type="float" office:value="' . $cellValue . '">'; - $data .= '' . $cellValue . ''; - $data .= ''; - } else if (empty($cellValue)) { - $data .= '/>'; - } else { - throw new InvalidArgumentException('Trying to add a value with an unsupported type: ' . gettype($cellValue)); - } - - return $data; - } - - /** - * Closes the worksheet - * - * @return void - */ - public function close() - { - if (!is_resource($this->sheetFilePointer)) { - return; - } - - fclose($this->sheetFilePointer); - } -} diff --git a/src/Spout/Writer/ODS/Writer.php b/src/Spout/Writer/ODS/Writer.php deleted file mode 100644 index 6571d00..0000000 --- a/src/Spout/Writer/ODS/Writer.php +++ /dev/null @@ -1,93 +0,0 @@ -throwIfWriterAlreadyOpened('Writer must be configured before opening it.'); - - $this->tempFolder = $tempFolder; - return $this; - } - - /** - * Configures the write and sets the current sheet pointer to a new sheet. - * - * @return void - * @throws \Box\Spout\Common\Exception\IOException If unable to open the file for writing - */ - protected function openWriter() - { - $tempFolder = ($this->tempFolder) ? : sys_get_temp_dir(); - $this->book = new Workbook($tempFolder, $this->shouldCreateNewSheetsAutomatically, $this->defaultRowStyle); - $this->book->addNewSheetAndMakeItCurrent(); - } - - /** - * @return Internal\Workbook The workbook representing the file to be written - */ - protected function getWorkbook() - { - return $this->book; - } - - /** - * Adds data to the currently opened writer. - * If shouldCreateNewSheetsAutomatically option is set to true, it will handle pagination - * with the creation of new worksheets if one worksheet has reached its maximum capicity. - * - * @param array $dataRow Array containing data to be written. - * Example $dataRow = ['data1', 1234, null, '', 'data5']; - * @param \Box\Spout\Writer\Style\Style $style Style to be applied to the row. - * @return void - * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the book is not created yet - * @throws \Box\Spout\Common\Exception\IOException If unable to write data - */ - protected function addRowToWriter(array $dataRow, $style) - { - $this->throwIfBookIsNotAvailable(); - $this->book->addRowToCurrentWorksheet($dataRow, $style); - } - - /** - * Closes the writer, preventing any additional writing. - * - * @return void - */ - protected function closeWriter() - { - if ($this->book) { - $this->book->close($this->filePointer); - } - } -} diff --git a/src/Spout/Writer/Style/Border.php b/src/Spout/Writer/Style/Border.php deleted file mode 100644 index 75f6a49..0000000 --- a/src/Spout/Writer/Style/Border.php +++ /dev/null @@ -1,85 +0,0 @@ -setParts($borderParts); - } - - /** - * @param $name The name of the border part - * @return null|BorderPart - */ - public function getPart($name) - { - return $this->hasPart($name) ? $this->parts[$name] : null; - } - - /** - * @param $name The name of the border part - * @return bool - */ - public function hasPart($name) - { - return isset($this->parts[$name]); - } - - /** - * @return array - */ - public function getParts() - { - return $this->parts; - } - - /** - * Set BorderParts - * @param array $parts - */ - public function setParts($parts) - { - unset($this->parts); - foreach ($parts as $part) { - $this->addPart($part); - } - } - - /** - * @param BorderPart $borderPart - * @return self - */ - public function addPart(BorderPart $borderPart) - { - $this->parts[$borderPart->getName()] = $borderPart; - return $this; - } -} diff --git a/src/Spout/Writer/Style/BorderBuilder.php b/src/Spout/Writer/Style/BorderBuilder.php deleted file mode 100644 index c0b8aea..0000000 --- a/src/Spout/Writer/Style/BorderBuilder.php +++ /dev/null @@ -1,75 +0,0 @@ -border = new Border(); - } - - /** - * @param string|void $color Border A RGB color code - * @param string|void $width Border width @see BorderPart::allowedWidths - * @param string|void $style Border style @see BorderPart::allowedStyles - * @return BorderBuilder - */ - public function setBorderTop($color = Color::BLACK, $width = Border::WIDTH_MEDIUM, $style = Border::STYLE_SOLID) - { - $this->border->addPart(new BorderPart(Border::TOP, $color, $width, $style)); - return $this; - } - - /** - * @param string|void $color Border A RGB color code - * @param string|void $width Border width @see BorderPart::allowedWidths - * @param string|void $style Border style @see BorderPart::allowedStyles - * @return BorderBuilder - */ - public function setBorderRight($color = Color::BLACK, $width = Border::WIDTH_MEDIUM, $style = Border::STYLE_SOLID) - { - $this->border->addPart(new BorderPart(Border::RIGHT, $color, $width, $style)); - return $this; - } - - /** - * @param string|void $color Border A RGB color code - * @param string|void $width Border width @see BorderPart::allowedWidths - * @param string|void $style Border style @see BorderPart::allowedStyles - * @return BorderBuilder - */ - public function setBorderBottom($color = Color::BLACK, $width = Border::WIDTH_MEDIUM, $style = Border::STYLE_SOLID) - { - $this->border->addPart(new BorderPart(Border::BOTTOM, $color, $width, $style)); - return $this; - } - - /** - * @param string|void $color Border A RGB color code - * @param string|void $width Border width @see BorderPart::allowedWidths - * @param string|void $style Border style @see BorderPart::allowedStyles - * @return BorderBuilder - */ - public function setBorderLeft($color = Color::BLACK, $width = Border::WIDTH_MEDIUM, $style = Border::STYLE_SOLID) - { - $this->border->addPart(new BorderPart(Border::LEFT, $color, $width, $style)); - return $this; - } - - /** - * @return Border - */ - public function build() - { - return $this->border; - } -} diff --git a/src/Spout/Writer/Style/BorderPart.php b/src/Spout/Writer/Style/BorderPart.php deleted file mode 100644 index 9ade797..0000000 --- a/src/Spout/Writer/Style/BorderPart.php +++ /dev/null @@ -1,184 +0,0 @@ -setName($name); - $this->setColor($color); - $this->setWidth($width); - $this->setStyle($style); - } - - /** - * @return string - */ - public function getName() - { - return $this->name; - } - - /** - * @param string $name The name of the border part @see BorderPart::$allowedNames - * @throws InvalidNameException - * @return void - */ - public function setName($name) - { - if (!in_array($name, self::$allowedNames)) { - throw new InvalidNameException($name); - } - $this->name = $name; - } - - /** - * @return string - */ - public function getStyle() - { - return $this->style; - } - - /** - * @param string $style The style of the border part @see BorderPart::$allowedStyles - * @throws InvalidStyleException - * @return void - */ - public function setStyle($style) - { - if (!in_array($style, self::$allowedStyles)) { - throw new InvalidStyleException($style); - } - $this->style = $style; - } - - /** - * @return string - */ - public function getColor() - { - return $this->color; - } - - /** - * @param string $color The color of the border part @see Color::rgb() - * @return void - */ - public function setColor($color) - { - $this->color = $color; - } - - /** - * @return string - */ - public function getWidth() - { - return $this->width; - } - - /** - * @param string $width The width of the border part @see BorderPart::$allowedWidths - * @throws InvalidWidthException - * @return void - */ - public function setWidth($width) - { - if (!in_array($width, self::$allowedWidths)) { - throw new InvalidWidthException($width); - } - $this->width = $width; - } - - /** - * @return array - */ - public static function getAllowedStyles() - { - return self::$allowedStyles; - } - - /** - * @return array - */ - public static function getAllowedNames() - { - return self::$allowedNames; - } - - /** - * @return array - */ - public static function getAllowedWidths() - { - return self::$allowedWidths; - } -} diff --git a/src/Spout/Writer/Style/Color.php b/src/Spout/Writer/Style/Color.php deleted file mode 100644 index 3c86eb7..0000000 --- a/src/Spout/Writer/Style/Color.php +++ /dev/null @@ -1,87 +0,0 @@ - 255) { - throw new InvalidColorException("The RGB components must be between 0 and 255. Received: $colorComponent"); - } - } - - /** - * Converts the color component to its corresponding hexadecimal value - * - * @param int $colorComponent Color component, 0 - 255 - * @return string Corresponding hexadecimal value, with a leading 0 if needed. E.g "0f", "2d" - */ - protected static function convertColorComponentToHex($colorComponent) - { - return str_pad(dechex($colorComponent), 2, '0', STR_PAD_LEFT); - } - - /** - * Returns the ARGB color of the given RGB color, - * assuming that alpha value is always 1. - * - * @param string $rgbColor RGB color like "FF08B2" - * @return string ARGB color - */ - public static function toARGB($rgbColor) - { - return 'FF' . $rgbColor; - } -} diff --git a/src/Spout/Writer/Style/Style.php b/src/Spout/Writer/Style/Style.php deleted file mode 100644 index b408ad3..0000000 --- a/src/Spout/Writer/Style/Style.php +++ /dev/null @@ -1,426 +0,0 @@ -id; - } - - /** - * @param int $id - * @return Style - */ - public function setId($id) - { - $this->id = $id; - return $this; - } - - /** - * @return Border - */ - public function getBorder() - { - return $this->border; - } - - /** - * @param Border $border - * @return Style - */ - public function setBorder(Border $border) - { - $this->shouldApplyBorder = true; - $this->border = $border; - return $this; - } - - /** - * @return bool - */ - public function shouldApplyBorder() - { - return $this->shouldApplyBorder; - } - - /** - * @return bool - */ - public function isFontBold() - { - return $this->fontBold; - } - - /** - * @return Style - */ - public function setFontBold() - { - $this->fontBold = true; - $this->hasSetFontBold = true; - $this->shouldApplyFont = true; - return $this; - } - - /** - * @return bool - */ - public function isFontItalic() - { - return $this->fontItalic; - } - - /** - * @return Style - */ - public function setFontItalic() - { - $this->fontItalic = true; - $this->hasSetFontItalic = true; - $this->shouldApplyFont = true; - return $this; - } - - /** - * @return bool - */ - public function isFontUnderline() - { - return $this->fontUnderline; - } - - /** - * @return Style - */ - public function setFontUnderline() - { - $this->fontUnderline = true; - $this->hasSetFontUnderline = true; - $this->shouldApplyFont = true; - return $this; - } - - /** - * @return bool - */ - public function isFontStrikethrough() - { - return $this->fontStrikethrough; - } - - /** - * @return Style - */ - public function setFontStrikethrough() - { - $this->fontStrikethrough = true; - $this->hasSetFontStrikethrough = true; - $this->shouldApplyFont = true; - return $this; - } - - /** - * @return int - */ - public function getFontSize() - { - return $this->fontSize; - } - - /** - * @param int $fontSize Font size, in pixels - * @return Style - */ - public function setFontSize($fontSize) - { - $this->fontSize = $fontSize; - $this->hasSetFontSize = true; - $this->shouldApplyFont = true; - return $this; - } - - /** - * @return string - */ - public function getFontColor() - { - return $this->fontColor; - } - - /** - * Sets the font color. - * - * @param string $fontColor ARGB color (@see Color) - * @return Style - */ - public function setFontColor($fontColor) - { - $this->fontColor = $fontColor; - $this->hasSetFontColor = true; - $this->shouldApplyFont = true; - return $this; - } - - /** - * @return string - */ - public function getFontName() - { - return $this->fontName; - } - - /** - * @param string $fontName Name of the font to use - * @return Style - */ - public function setFontName($fontName) - { - $this->fontName = $fontName; - $this->hasSetFontName = true; - $this->shouldApplyFont = true; - return $this; - } - - /** - * @return bool - */ - public function shouldWrapText() - { - return $this->shouldWrapText; - } - - /** - * @param bool|void $shouldWrap Should the text be wrapped - * @return Style - */ - public function setShouldWrapText($shouldWrap = true) - { - $this->shouldWrapText = $shouldWrap; - $this->hasSetWrapText = true; - return $this; - } - - /** - * @return bool - */ - public function hasSetWrapText() - { - return $this->hasSetWrapText; - } - - /** - * @return bool Whether specific font properties should be applied - */ - public function shouldApplyFont() - { - return $this->shouldApplyFont; - } - - /** - * Sets the background color - * @param string $color ARGB color (@see Color) - * @return Style - */ - public function setBackgroundColor($color) - { - $this->hasSetBackgroundColor = true; - $this->backgroundColor = $color; - return $this; - } - - /** - * @return string - */ - public function getBackgroundColor() - { - return $this->backgroundColor; - } - - /** - * - * @return bool Whether the background color should be applied - */ - public function shouldApplyBackgroundColor() - { - return $this->hasSetBackgroundColor; - } - - /** - * Serializes the style for future comparison with other styles. - * The ID is excluded from the comparison, as we only care about - * actual style properties. - * - * @return string The serialized style - */ - public function serialize() - { - // In order to be able to properly compare style, set static ID value - $currentId = $this->id; - $this->setId(0); - - $serializedStyle = serialize($this); - - $this->setId($currentId); - - return $serializedStyle; - } - - /** - * Merges the current style with the given style, using the given style as a base. This means that: - * - if current style and base style both have property A set, use current style property's value - * - if current style has property A set but base style does not, use current style property's value - * - if base style has property A set but current style does not, use base style property's value - * - * @NOTE: This function returns a new style. - * - * @param Style $baseStyle - * @return Style New style corresponding to the merge of the 2 styles - */ - public function mergeWith($baseStyle) - { - $mergedStyle = clone $this; - - $this->mergeFontStyles($mergedStyle, $baseStyle); - $this->mergeOtherFontProperties($mergedStyle, $baseStyle); - $this->mergeCellProperties($mergedStyle, $baseStyle); - - return $mergedStyle; - } - - /** - * @param Style $styleToUpdate (passed as reference) - * @param Style $baseStyle - * @return void - */ - private function mergeFontStyles($styleToUpdate, $baseStyle) - { - if (!$this->hasSetFontBold && $baseStyle->isFontBold()) { - $styleToUpdate->setFontBold(); - } - if (!$this->hasSetFontItalic && $baseStyle->isFontItalic()) { - $styleToUpdate->setFontItalic(); - } - if (!$this->hasSetFontUnderline && $baseStyle->isFontUnderline()) { - $styleToUpdate->setFontUnderline(); - } - if (!$this->hasSetFontStrikethrough && $baseStyle->isFontStrikethrough()) { - $styleToUpdate->setFontStrikethrough(); - } - } - - /** - * @param Style $styleToUpdate Style to update (passed as reference) - * @param Style $baseStyle - * @return void - */ - private function mergeOtherFontProperties($styleToUpdate, $baseStyle) - { - if (!$this->hasSetFontSize && $baseStyle->getFontSize() !== self::DEFAULT_FONT_SIZE) { - $styleToUpdate->setFontSize($baseStyle->getFontSize()); - } - if (!$this->hasSetFontColor && $baseStyle->getFontColor() !== self::DEFAULT_FONT_COLOR) { - $styleToUpdate->setFontColor($baseStyle->getFontColor()); - } - if (!$this->hasSetFontName && $baseStyle->getFontName() !== self::DEFAULT_FONT_NAME) { - $styleToUpdate->setFontName($baseStyle->getFontName()); - } - } - - /** - * @param Style $styleToUpdate Style to update (passed as reference) - * @param Style $baseStyle - * @return void - */ - private function mergeCellProperties($styleToUpdate, $baseStyle) - { - if (!$this->hasSetWrapText && $baseStyle->shouldWrapText()) { - $styleToUpdate->setShouldWrapText(); - } - if (!$this->getBorder() && $baseStyle->shouldApplyBorder()) { - $styleToUpdate->setBorder($baseStyle->getBorder()); - } - if (!$this->hasSetBackgroundColor && $baseStyle->shouldApplyBackgroundColor()) { - $styleToUpdate->setBackgroundColor($baseStyle->getBackgroundColor()); - } - } -} diff --git a/src/Spout/Writer/Style/StyleBuilder.php b/src/Spout/Writer/Style/StyleBuilder.php deleted file mode 100644 index 2676cbe..0000000 --- a/src/Spout/Writer/Style/StyleBuilder.php +++ /dev/null @@ -1,159 +0,0 @@ -style = new Style(); - } - - /** - * Makes the font bold. - * - * @api - * @return StyleBuilder - */ - public function setFontBold() - { - $this->style->setFontBold(); - return $this; - } - - /** - * Makes the font italic. - * - * @api - * @return StyleBuilder - */ - public function setFontItalic() - { - $this->style->setFontItalic(); - return $this; - } - - /** - * Makes the font underlined. - * - * @api - * @return StyleBuilder - */ - public function setFontUnderline() - { - $this->style->setFontUnderline(); - return $this; - } - - /** - * Makes the font struck through. - * - * @api - * @return StyleBuilder - */ - public function setFontStrikethrough() - { - $this->style->setFontStrikethrough(); - return $this; - } - - /** - * Sets the font size. - * - * @api - * @param int $fontSize Font size, in pixels - * @return StyleBuilder - */ - public function setFontSize($fontSize) - { - $this->style->setFontSize($fontSize); - return $this; - } - - /** - * Sets the font color. - * - * @api - * @param string $fontColor ARGB color (@see Color) - * @return StyleBuilder - */ - public function setFontColor($fontColor) - { - $this->style->setFontColor($fontColor); - return $this; - } - - /** - * Sets the font name. - * - * @api - * @param string $fontName Name of the font to use - * @return StyleBuilder - */ - public function setFontName($fontName) - { - $this->style->setFontName($fontName); - return $this; - } - - /** - * Makes the text wrap in the cell if requested - * - * @api - * @param bool $shouldWrap Should the text be wrapped - * @return StyleBuilder - */ - public function setShouldWrapText($shouldWrap = true) - { - $this->style->setShouldWrapText($shouldWrap); - return $this; - } - - /** - * Set a border - * - * @param Border $border - * @return $this - */ - public function setBorder(Border $border) - { - $this->style->setBorder($border); - return $this; - } - - /** - * Sets a background color - * - * @api - * @param string $color ARGB color (@see Color) - * @return StyleBuilder - */ - public function setBackgroundColor($color) - { - $this->style->setBackgroundColor($color); - return $this; - } - - /** - * Returns the configured style. The style is cached and can be reused. - * - * @api - * @return Style - */ - public function build() - { - return $this->style; - } -} diff --git a/src/Spout/Writer/WriterFactory.php b/src/Spout/Writer/WriterFactory.php deleted file mode 100644 index 7588940..0000000 --- a/src/Spout/Writer/WriterFactory.php +++ /dev/null @@ -1,48 +0,0 @@ -setGlobalFunctionsHelper(new GlobalFunctionsHelper()); - - return $writer; - } -} diff --git a/src/Spout/Writer/WriterInterface.php b/src/Spout/Writer/WriterInterface.php deleted file mode 100644 index e2d9f8d..0000000 --- a/src/Spout/Writer/WriterInterface.php +++ /dev/null @@ -1,91 +0,0 @@ - [ - Border::WIDTH_THIN => 'thin', - Border::WIDTH_MEDIUM => 'medium', - Border::WIDTH_THICK => 'thick' - ], - Border::STYLE_DOTTED => [ - Border::WIDTH_THIN => 'dotted', - Border::WIDTH_MEDIUM => 'dotted', - Border::WIDTH_THICK => 'dotted', - ], - Border::STYLE_DASHED => [ - Border::WIDTH_THIN => 'dashed', - Border::WIDTH_MEDIUM => 'mediumDashed', - Border::WIDTH_THICK => 'mediumDashed', - ], - Border::STYLE_DOUBLE => [ - Border::WIDTH_THIN => 'double', - Border::WIDTH_MEDIUM => 'double', - Border::WIDTH_THICK => 'double', - ], - Border::STYLE_NONE => [ - Border::WIDTH_THIN => 'none', - Border::WIDTH_MEDIUM => 'none', - Border::WIDTH_THICK => 'none', - ], - ]; - - /** - * @param BorderPart $borderPart - * @return string - */ - public static function serializeBorderPart(BorderPart $borderPart) - { - $borderStyle = self::getBorderStyle($borderPart); - - $colorEl = $borderPart->getColor() ? sprintf('', $borderPart->getColor()) : ''; - $partEl = sprintf( - '<%s style="%s">%s', - $borderPart->getName(), - $borderStyle, - $colorEl, - $borderPart->getName() - ); - - return $partEl . PHP_EOL; - } - - /** - * Get the style definition from the style map - * - * @param BorderPart $borderPart - * @return string - */ - protected static function getBorderStyle(BorderPart $borderPart) - { - return self::$xlsxStyleMap[$borderPart->getStyle()][$borderPart->getWidth()]; - } -} diff --git a/src/Spout/Writer/XLSX/Helper/FileSystemHelper.php b/src/Spout/Writer/XLSX/Helper/FileSystemHelper.php deleted file mode 100644 index 86515f3..0000000 --- a/src/Spout/Writer/XLSX/Helper/FileSystemHelper.php +++ /dev/null @@ -1,371 +0,0 @@ -rootFolder; - } - - /** - * @return string - */ - public function getXlFolder() - { - return $this->xlFolder; - } - - /** - * @return string - */ - public function getXlWorksheetsFolder() - { - return $this->xlWorksheetsFolder; - } - - /** - * Creates all the folders needed to create a XLSX file, as well as the files that won't change. - * - * @return void - * @throws \Box\Spout\Common\Exception\IOException If unable to create at least one of the base folders - */ - public function createBaseFilesAndFolders() - { - $this - ->createRootFolder() - ->createRelsFolderAndFile() - ->createDocPropsFolderAndFiles() - ->createXlFolderAndSubFolders(); - } - - /** - * Creates the folder that will be used as root - * - * @return FileSystemHelper - * @throws \Box\Spout\Common\Exception\IOException If unable to create the folder - */ - protected function createRootFolder() - { - $this->rootFolder = $this->createFolder($this->baseFolderRealPath, uniqid('xlsx', true)); - return $this; - } - - /** - * Creates the "_rels" folder under the root folder as well as the ".rels" file in it - * - * @return FileSystemHelper - * @throws \Box\Spout\Common\Exception\IOException If unable to create the folder or the ".rels" file - */ - protected function createRelsFolderAndFile() - { - $this->relsFolder = $this->createFolder($this->rootFolder, self::RELS_FOLDER_NAME); - - $this->createRelsFile(); - - return $this; - } - - /** - * Creates the ".rels" file under the "_rels" folder (under root) - * - * @return FileSystemHelper - * @throws \Box\Spout\Common\Exception\IOException If unable to create the file - */ - protected function createRelsFile() - { - $relsFileContents = << - - - - - -EOD; - - $this->createFileWithContents($this->relsFolder, self::RELS_FILE_NAME, $relsFileContents); - - return $this; - } - - /** - * Creates the "docProps" folder under the root folder as well as the "app.xml" and "core.xml" files in it - * - * @return FileSystemHelper - * @throws \Box\Spout\Common\Exception\IOException If unable to create the folder or one of the files - */ - protected function createDocPropsFolderAndFiles() - { - $this->docPropsFolder = $this->createFolder($this->rootFolder, self::DOC_PROPS_FOLDER_NAME); - - $this->createAppXmlFile(); - $this->createCoreXmlFile(); - - return $this; - } - - /** - * Creates the "app.xml" file under the "docProps" folder - * - * @return FileSystemHelper - * @throws \Box\Spout\Common\Exception\IOException If unable to create the file - */ - protected function createAppXmlFile() - { - $appName = self::APP_NAME; - $appXmlFileContents = << - - $appName - 0 - -EOD; - - $this->createFileWithContents($this->docPropsFolder, self::APP_XML_FILE_NAME, $appXmlFileContents); - - return $this; - } - - /** - * Creates the "core.xml" file under the "docProps" folder - * - * @return FileSystemHelper - * @throws \Box\Spout\Common\Exception\IOException If unable to create the file - */ - protected function createCoreXmlFile() - { - $createdDate = (new \DateTime())->format(\DateTime::W3C); - $coreXmlFileContents = << - - $createdDate - $createdDate - 0 - -EOD; - - $this->createFileWithContents($this->docPropsFolder, self::CORE_XML_FILE_NAME, $coreXmlFileContents); - - return $this; - } - - /** - * Creates the "xl" folder under the root folder as well as its subfolders - * - * @return FileSystemHelper - * @throws \Box\Spout\Common\Exception\IOException If unable to create at least one of the folders - */ - protected function createXlFolderAndSubFolders() - { - $this->xlFolder = $this->createFolder($this->rootFolder, self::XL_FOLDER_NAME); - $this->createXlRelsFolder(); - $this->createXlWorksheetsFolder(); - - return $this; - } - - /** - * Creates the "_rels" folder under the "xl" folder - * - * @return FileSystemHelper - * @throws \Box\Spout\Common\Exception\IOException If unable to create the folder - */ - protected function createXlRelsFolder() - { - $this->xlRelsFolder = $this->createFolder($this->xlFolder, self::RELS_FOLDER_NAME); - return $this; - } - - /** - * Creates the "worksheets" folder under the "xl" folder - * - * @return FileSystemHelper - * @throws \Box\Spout\Common\Exception\IOException If unable to create the folder - */ - protected function createXlWorksheetsFolder() - { - $this->xlWorksheetsFolder = $this->createFolder($this->xlFolder, self::WORKSHEETS_FOLDER_NAME); - return $this; - } - - /** - * Creates the "[Content_Types].xml" file under the root folder - * - * @param Worksheet[] $worksheets - * @return FileSystemHelper - */ - public function createContentTypesFile($worksheets) - { - $contentTypesXmlFileContents = << - - - - -EOD; - - /** @var Worksheet $worksheet */ - foreach ($worksheets as $worksheet) { - $contentTypesXmlFileContents .= ''; - } - - $contentTypesXmlFileContents .= << - - - - -EOD; - - $this->createFileWithContents($this->rootFolder, self::CONTENT_TYPES_XML_FILE_NAME, $contentTypesXmlFileContents); - - return $this; - } - - /** - * Creates the "workbook.xml" file under the "xl" folder - * - * @param Worksheet[] $worksheets - * @return FileSystemHelper - */ - public function createWorkbookFile($worksheets) - { - $workbookXmlFileContents = << - - -EOD; - - /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ - $escaper = \Box\Spout\Common\Escaper\XLSX::getInstance(); - - /** @var Worksheet $worksheet */ - foreach ($worksheets as $worksheet) { - $worksheetName = $worksheet->getExternalSheet()->getName(); - $worksheetId = $worksheet->getId(); - $workbookXmlFileContents .= ''; - } - - $workbookXmlFileContents .= << - -EOD; - - $this->createFileWithContents($this->xlFolder, self::WORKBOOK_XML_FILE_NAME, $workbookXmlFileContents); - - return $this; - } - - /** - * Creates the "workbook.xml.res" file under the "xl/_res" folder - * - * @param Worksheet[] $worksheets - * @return FileSystemHelper - */ - public function createWorkbookRelsFile($worksheets) - { - $workbookRelsXmlFileContents = << - - - -EOD; - - /** @var Worksheet $worksheet */ - foreach ($worksheets as $worksheet) { - $worksheetId = $worksheet->getId(); - $workbookRelsXmlFileContents .= ''; - } - - $workbookRelsXmlFileContents .= ''; - - $this->createFileWithContents($this->xlRelsFolder, self::WORKBOOK_RELS_XML_FILE_NAME, $workbookRelsXmlFileContents); - - return $this; - } - - /** - * Creates the "styles.xml" file under the "xl" folder - * - * @param StyleHelper $styleHelper - * @return FileSystemHelper - */ - public function createStylesFile($styleHelper) - { - $stylesXmlFileContents = $styleHelper->getStylesXMLFileContent(); - $this->createFileWithContents($this->xlFolder, self::STYLES_XML_FILE_NAME, $stylesXmlFileContents); - - return $this; - } - - /** - * Zips the root folder and streams the contents of the zip into the given stream - * - * @param resource $streamPointer Pointer to the stream to copy the zip - * @return void - */ - public function zipRootFolderAndCopyToStream($streamPointer) - { - $zipHelper = new ZipHelper($this->rootFolder); - - // In order to have the file's mime type detected properly, files need to be added - // to the zip file in a particular order. - // "[Content_Types].xml" then at least 2 files located in "xl" folder should be zipped first. - $zipHelper->addFileToArchive($this->rootFolder, self::CONTENT_TYPES_XML_FILE_NAME); - $zipHelper->addFileToArchive($this->rootFolder, self::XL_FOLDER_NAME . '/' . self::WORKBOOK_XML_FILE_NAME); - $zipHelper->addFileToArchive($this->rootFolder, self::XL_FOLDER_NAME . '/' . self::STYLES_XML_FILE_NAME); - - $zipHelper->addFolderToArchive($this->rootFolder, ZipHelper::EXISTING_FILES_SKIP); - $zipHelper->closeArchiveAndCopyToStream($streamPointer); - - // once the zip is copied, remove it - $this->deleteFile($zipHelper->getZipFilePath()); - } -} diff --git a/src/Spout/Writer/XLSX/Helper/SharedStringsHelper.php b/src/Spout/Writer/XLSX/Helper/SharedStringsHelper.php deleted file mode 100644 index 292b663..0000000 --- a/src/Spout/Writer/XLSX/Helper/SharedStringsHelper.php +++ /dev/null @@ -1,107 +0,0 @@ - -sharedStringsFilePointer = fopen($sharedStringsFilePath, 'w'); - - $this->throwIfSharedStringsFilePointerIsNotAvailable(); - - // the headers is split into different parts so that we can fseek and put in the correct count and uniqueCount later - $header = self::SHARED_STRINGS_XML_FILE_FIRST_PART_HEADER . ' ' . self::DEFAULT_STRINGS_COUNT_PART . '>'; - fwrite($this->sharedStringsFilePointer, $header); - - /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ - $this->stringsEscaper = \Box\Spout\Common\Escaper\XLSX::getInstance(); - } - - /** - * Checks if the book has been created. Throws an exception if not created yet. - * - * @return void - * @throws \Box\Spout\Common\Exception\IOException If the sheet data file cannot be opened for writing - */ - protected function throwIfSharedStringsFilePointerIsNotAvailable() - { - if (!$this->sharedStringsFilePointer) { - throw new IOException('Unable to open shared strings file for writing.'); - } - } - - /** - * Writes the given string into the sharedStrings.xml file. - * Starting and ending whitespaces are preserved. - * - * @param string $string - * @return int ID of the written shared string - */ - public function writeString($string) - { - fwrite($this->sharedStringsFilePointer, '' . $this->stringsEscaper->escape($string) . ''); - $this->numSharedStrings++; - - // Shared string ID is zero-based - return ($this->numSharedStrings - 1); - } - - /** - * Finishes writing the data in the sharedStrings.xml file and closes the file. - * - * @return void - */ - public function close() - { - if (!is_resource($this->sharedStringsFilePointer)) { - return; - } - - fwrite($this->sharedStringsFilePointer, ''); - - // Replace the default strings count with the actual number of shared strings in the file header - $firstPartHeaderLength = strlen(self::SHARED_STRINGS_XML_FILE_FIRST_PART_HEADER); - $defaultStringsCountPartLength = strlen(self::DEFAULT_STRINGS_COUNT_PART); - - // Adding 1 to take into account the space between the last xml attribute and "count" - fseek($this->sharedStringsFilePointer, $firstPartHeaderLength + 1); - fwrite($this->sharedStringsFilePointer, sprintf("%-{$defaultStringsCountPartLength}s", 'count="' . $this->numSharedStrings . '" uniqueCount="' . $this->numSharedStrings . '"')); - - fclose($this->sharedStringsFilePointer); - } -} diff --git a/src/Spout/Writer/XLSX/Helper/StyleHelper.php b/src/Spout/Writer/XLSX/Helper/StyleHelper.php deleted file mode 100644 index 4a13c95..0000000 --- a/src/Spout/Writer/XLSX/Helper/StyleHelper.php +++ /dev/null @@ -1,344 +0,0 @@ - [FILL_ID] maps a style to a fill declaration - */ - protected $styleIdToFillMappingTable = []; - - /** - * Excel preserves two default fills with index 0 and 1 - * Since Excel is the dominant vendor - we play along here - * - * @var int The fill index counter for custom fills. - */ - protected $fillIndex = 2; - - /** - * @var array - */ - protected $registeredBorders = []; - - /** - * @var array [STYLE_ID] => [BORDER_ID] maps a style to a border declaration - */ - protected $styleIdToBorderMappingTable = []; - - /** - * XLSX specific operations on the registered styles - * - * @param \Box\Spout\Writer\Style\Style $style - * @return \Box\Spout\Writer\Style\Style - */ - public function registerStyle($style) - { - $registeredStyle = parent::registerStyle($style); - $this->registerFill($registeredStyle); - $this->registerBorder($registeredStyle); - return $registeredStyle; - } - - /** - * Register a fill definition - * - * @param \Box\Spout\Writer\Style\Style $style - */ - protected function registerFill($style) - { - $styleId = $style->getId(); - - // Currently - only solid backgrounds are supported - // so $backgroundColor is a scalar value (RGB Color) - $backgroundColor = $style->getBackgroundColor(); - - if ($backgroundColor) { - $isBackgroundColorRegistered = isset($this->registeredFills[$backgroundColor]); - - // We need to track the already registered background definitions - if ($isBackgroundColorRegistered) { - $registeredStyleId = $this->registeredFills[$backgroundColor]; - $registeredFillId = $this->styleIdToFillMappingTable[$registeredStyleId]; - $this->styleIdToFillMappingTable[$styleId] = $registeredFillId; - } else { - $this->registeredFills[$backgroundColor] = $styleId; - $this->styleIdToFillMappingTable[$styleId] = $this->fillIndex++; - } - - } else { - // The fillId maps a style to a fill declaration - // When there is no background color definition - we default to 0 - $this->styleIdToFillMappingTable[$styleId] = 0; - } - } - - /** - * Register a border definition - * - * @param \Box\Spout\Writer\Style\Style $style - */ - protected function registerBorder($style) - { - $styleId = $style->getId(); - - if ($style->shouldApplyBorder()) { - $border = $style->getBorder(); - $serializedBorder = serialize($border); - - $isBorderAlreadyRegistered = isset($this->registeredBorders[$serializedBorder]); - - if ($isBorderAlreadyRegistered) { - $registeredStyleId = $this->registeredBorders[$serializedBorder]; - $registeredBorderId = $this->styleIdToBorderMappingTable[$registeredStyleId]; - $this->styleIdToBorderMappingTable[$styleId] = $registeredBorderId; - } else { - $this->registeredBorders[$serializedBorder] = $styleId; - $this->styleIdToBorderMappingTable[$styleId] = count($this->registeredBorders); - } - - } else { - // If no border should be applied - the mapping is the default border: 0 - $this->styleIdToBorderMappingTable[$styleId] = 0; - } - } - - - /** - * For empty cells, we can specify a style or not. If no style are specified, - * then the software default will be applied. But sometimes, it may be useful - * to override this default style, for instance if the cell should have a - * background color different than the default one or some borders - * (fonts property don't really matter here). - * - * @param int $styleId - * @return bool Whether the cell should define a custom style - */ - public function shouldApplyStyleOnEmptyCell($styleId) - { - $hasStyleCustomFill = (isset($this->styleIdToFillMappingTable[$styleId]) && $this->styleIdToFillMappingTable[$styleId] !== 0); - $hasStyleCustomBorders = (isset($this->styleIdToBorderMappingTable[$styleId]) && $this->styleIdToBorderMappingTable[$styleId] !== 0); - - return ($hasStyleCustomFill || $hasStyleCustomBorders); - } - - - /** - * Returns the content of the "styles.xml" file, given a list of styles. - * - * @return string - */ - public function getStylesXMLFileContent() - { - $content = << - -EOD; - - $content .= $this->getFontsSectionContent(); - $content .= $this->getFillsSectionContent(); - $content .= $this->getBordersSectionContent(); - $content .= $this->getCellStyleXfsSectionContent(); - $content .= $this->getCellXfsSectionContent(); - $content .= $this->getCellStylesSectionContent(); - - $content .= << -EOD; - - return $content; - } - - /** - * Returns the content of the "" section. - * - * @return string - */ - protected function getFontsSectionContent() - { - $content = ''; - - /** @var \Box\Spout\Writer\Style\Style $style */ - foreach ($this->getRegisteredStyles() as $style) { - $content .= ''; - - $content .= ''; - $content .= ''; - $content .= ''; - - if ($style->isFontBold()) { - $content .= ''; - } - if ($style->isFontItalic()) { - $content .= ''; - } - if ($style->isFontUnderline()) { - $content .= ''; - } - if ($style->isFontStrikethrough()) { - $content .= ''; - } - - $content .= ''; - } - - $content .= ''; - - return $content; - } - - /** - * Returns the content of the "" section. - * - * @return string - */ - protected function getFillsSectionContent() - { - // Excel reserves two default fills - $fillsCount = count($this->registeredFills) + 2; - $content = sprintf('', $fillsCount); - - $content .= ''; - $content .= ''; - - // The other fills are actually registered by setting a background color - foreach ($this->registeredFills as $styleId) { - /** @var Style $style */ - $style = $this->styleIdToStyleMappingTable[$styleId]; - - $backgroundColor = $style->getBackgroundColor(); - $content .= sprintf( - '', - $backgroundColor - ); - } - - $content .= ''; - - return $content; - } - - /** - * Returns the content of the "" section. - * - * @return string - */ - protected function getBordersSectionContent() - { - - // There is one default border with index 0 - $borderCount = count($this->registeredBorders) + 1; - - $content = ''; - - // Default border starting at index 0 - $content .= ''; - - foreach ($this->registeredBorders as $styleId) { - /** @var \Box\Spout\Writer\Style\Style $style */ - $style = $this->styleIdToStyleMappingTable[$styleId]; - $border = $style->getBorder(); - $content .= ''; - - // @link https://github.com/box/spout/issues/271 - $sortOrder = ['left', 'right', 'top', 'bottom']; - - foreach ($sortOrder as $partName) { - if ($border->hasPart($partName)) { - /** @var $part \Box\Spout\Writer\Style\BorderPart */ - $part = $border->getPart($partName); - $content .= BorderHelper::serializeBorderPart($part); - } - - } - - $content .= ''; - } - - $content .= ''; - - return $content; - } - - /** - * Returns the content of the "" section. - * - * @return string - */ - protected function getCellStyleXfsSectionContent() - { - return << - - -EOD; - } - - /** - * Returns the content of the "" section. - * - * @return string - */ - protected function getCellXfsSectionContent() - { - $registeredStyles = $this->getRegisteredStyles(); - - $content = ''; - - foreach ($registeredStyles as $style) { - $styleId = $style->getId(); - $fillId = $this->styleIdToFillMappingTable[$styleId]; - $borderId = $this->styleIdToBorderMappingTable[$styleId]; - - $content .= 'shouldApplyFont()) { - $content .= ' applyFont="1"'; - } - - $content .= sprintf(' applyBorder="%d"', $style->shouldApplyBorder() ? 1 : 0); - - if ($style->shouldWrapText()) { - $content .= ' applyAlignment="1">'; - $content .= ''; - $content .= ''; - } else { - $content .= '/>'; - } - } - - $content .= ''; - - return $content; - } - - /** - * Returns the content of the "" section. - * - * @return string - */ - protected function getCellStylesSectionContent() - { - return << - - -EOD; - } -} diff --git a/src/Spout/Writer/XLSX/Internal/Workbook.php b/src/Spout/Writer/XLSX/Internal/Workbook.php deleted file mode 100644 index bcdce7f..0000000 --- a/src/Spout/Writer/XLSX/Internal/Workbook.php +++ /dev/null @@ -1,135 +0,0 @@ -shouldUseInlineStrings = $shouldUseInlineStrings; - - $this->fileSystemHelper = new FileSystemHelper($tempFolder); - $this->fileSystemHelper->createBaseFilesAndFolders(); - - $this->styleHelper = new StyleHelper($defaultRowStyle); - - // This helper will be shared by all sheets - $xlFolder = $this->fileSystemHelper->getXlFolder(); - $this->sharedStringsHelper = new SharedStringsHelper($xlFolder); - } - - /** - * @return \Box\Spout\Writer\XLSX\Helper\StyleHelper Helper to apply styles to XLSX files - */ - protected function getStyleHelper() - { - return $this->styleHelper; - } - - /** - * @return int Maximum number of rows/columns a sheet can contain - */ - protected function getMaxRowsPerWorksheet() - { - return self::$maxRowsPerWorksheet; - } - - /** - * Creates a new sheet in the workbook. The current sheet remains unchanged. - * - * @return Worksheet The created sheet - * @throws \Box\Spout\Common\Exception\IOException If unable to open the sheet for writing - */ - public function addNewSheet() - { - $newSheetIndex = count($this->worksheets); - $sheet = new Sheet($newSheetIndex, $this->internalId); - - $worksheetFilesFolder = $this->fileSystemHelper->getXlWorksheetsFolder(); - $worksheet = new Worksheet($sheet, $worksheetFilesFolder, $this->sharedStringsHelper, $this->styleHelper, $this->shouldUseInlineStrings); - $this->worksheets[] = $worksheet; - - return $worksheet; - } - - /** - * Closes the workbook and all its associated sheets. - * All the necessary files are written to disk and zipped together to create the XLSX file. - * All the temporary files are then deleted. - * - * @param resource $finalFilePointer Pointer to the XLSX that will be created - * @return void - */ - public function close($finalFilePointer) - { - /** @var Worksheet[] $worksheets */ - $worksheets = $this->worksheets; - - foreach ($worksheets as $worksheet) { - $worksheet->close(); - } - - $this->sharedStringsHelper->close(); - - // Finish creating all the necessary files before zipping everything together - $this->fileSystemHelper - ->createContentTypesFile($worksheets) - ->createWorkbookFile($worksheets) - ->createWorkbookRelsFile($worksheets) - ->createStylesFile($this->styleHelper) - ->zipRootFolderAndCopyToStream($finalFilePointer); - - $this->cleanupTempFolder(); - } - - /** - * Deletes the root folder created in the temp folder and all its contents. - * - * @return void - */ - protected function cleanupTempFolder() - { - $xlsxRootFolder = $this->fileSystemHelper->getRootFolder(); - $this->fileSystemHelper->deleteFolderRecursively($xlsxRootFolder); - } -} diff --git a/src/Spout/Writer/XLSX/Internal/Worksheet.php b/src/Spout/Writer/XLSX/Internal/Worksheet.php deleted file mode 100644 index b5a3dc7..0000000 --- a/src/Spout/Writer/XLSX/Internal/Worksheet.php +++ /dev/null @@ -1,275 +0,0 @@ - - -EOD; - - /** @var \Box\Spout\Writer\Common\Sheet The "external" sheet */ - protected $externalSheet; - - /** @var string Path to the XML file that will contain the sheet data */ - protected $worksheetFilePath; - - /** @var \Box\Spout\Writer\XLSX\Helper\SharedStringsHelper Helper to write shared strings */ - protected $sharedStringsHelper; - - /** @var \Box\Spout\Writer\XLSX\Helper\StyleHelper Helper to work with styles */ - protected $styleHelper; - - /** @var bool Whether inline or shared strings should be used */ - protected $shouldUseInlineStrings; - - /** @var \Box\Spout\Common\Escaper\XLSX Strings escaper */ - protected $stringsEscaper; - - /** @var \Box\Spout\Common\Helper\StringHelper String helper */ - protected $stringHelper; - - /** @var Resource Pointer to the sheet data file (e.g. xl/worksheets/sheet1.xml) */ - protected $sheetFilePointer; - - /** @var int Index of the last written row */ - protected $lastWrittenRowIndex = 0; - - /** - * @param \Box\Spout\Writer\Common\Sheet $externalSheet The associated "external" sheet - * @param string $worksheetFilesFolder Temporary folder where the files to create the XLSX will be stored - * @param \Box\Spout\Writer\XLSX\Helper\SharedStringsHelper $sharedStringsHelper Helper for shared strings - * @param \Box\Spout\Writer\XLSX\Helper\StyleHelper Helper to work with styles - * @param bool $shouldUseInlineStrings Whether inline or shared strings should be used - * @throws \Box\Spout\Common\Exception\IOException If the sheet data file cannot be opened for writing - */ - public function __construct($externalSheet, $worksheetFilesFolder, $sharedStringsHelper, $styleHelper, $shouldUseInlineStrings) - { - $this->externalSheet = $externalSheet; - $this->sharedStringsHelper = $sharedStringsHelper; - $this->styleHelper = $styleHelper; - $this->shouldUseInlineStrings = $shouldUseInlineStrings; - - /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ - $this->stringsEscaper = \Box\Spout\Common\Escaper\XLSX::getInstance(); - $this->stringHelper = new StringHelper(); - - $this->worksheetFilePath = $worksheetFilesFolder . '/' . strtolower($this->externalSheet->getName()) . '.xml'; - $this->startSheet(); - } - - /** - * Prepares the worksheet to accept data - * - * @return void - * @throws \Box\Spout\Common\Exception\IOException If the sheet data file cannot be opened for writing - */ - protected function startSheet() - { - $this->sheetFilePointer = fopen($this->worksheetFilePath, 'w'); - $this->throwIfSheetFilePointerIsNotAvailable(); - - fwrite($this->sheetFilePointer, self::SHEET_XML_FILE_HEADER); - fwrite($this->sheetFilePointer, ''); - } - - /** - * Checks if the book has been created. Throws an exception if not created yet. - * - * @return void - * @throws \Box\Spout\Common\Exception\IOException If the sheet data file cannot be opened for writing - */ - protected function throwIfSheetFilePointerIsNotAvailable() - { - if (!$this->sheetFilePointer) { - throw new IOException('Unable to open sheet for writing.'); - } - } - - /** - * @return \Box\Spout\Writer\Common\Sheet The "external" sheet - */ - public function getExternalSheet() - { - return $this->externalSheet; - } - - /** - * @return int The index of the last written row - */ - public function getLastWrittenRowIndex() - { - return $this->lastWrittenRowIndex; - } - - /** - * @return int The ID of the worksheet - */ - public function getId() - { - // sheet index is zero-based, while ID is 1-based - return $this->externalSheet->getIndex() + 1; - } - - /** - * Adds data to the worksheet. - * - * @param array $dataRow Array containing data to be written. Cannot be empty. - * Example $dataRow = ['data1', 1234, null, '', 'data5']; - * @param \Box\Spout\Writer\Style\Style $style Style to be applied to the row. NULL means use default style. - * @return void - * @throws \Box\Spout\Common\Exception\IOException If the data cannot be written - * @throws \Box\Spout\Common\Exception\InvalidArgumentException If a cell value's type is not supported - */ - public function addRow($dataRow, $style) - { - if (!$this->isEmptyRow($dataRow)) { - $this->addNonEmptyRow($dataRow, $style); - } - - $this->lastWrittenRowIndex++; - } - - /** - * Returns whether the given row is empty - * - * @param array $dataRow Array containing data to be written. Cannot be empty. - * Example $dataRow = ['data1', 1234, null, '', 'data5']; - * @return bool Whether the given row is empty - */ - private function isEmptyRow($dataRow) - { - $numCells = count($dataRow); - // using "reset()" instead of "$dataRow[0]" because $dataRow can be an associative array - return ($numCells === 1 && CellHelper::isEmpty(reset($dataRow))); - } - - /** - * Adds non empty row to the worksheet. - * - * @param array $dataRow Array containing data to be written. Cannot be empty. - * Example $dataRow = ['data1', 1234, null, '', 'data5']; - * @param \Box\Spout\Writer\Style\Style $style Style to be applied to the row. NULL means use default style. - * @return void - * @throws \Box\Spout\Common\Exception\IOException If the data cannot be written - * @throws \Box\Spout\Common\Exception\InvalidArgumentException If a cell value's type is not supported - */ - private function addNonEmptyRow($dataRow, $style) - { - $cellNumber = 0; - $rowIndex = $this->lastWrittenRowIndex + 1; - $numCells = count($dataRow); - - $rowXML = ''; - - foreach($dataRow as $cellValue) { - $rowXML .= $this->getCellXML($rowIndex, $cellNumber, $cellValue, $style->getId()); - $cellNumber++; - } - - $rowXML .= ''; - - $wasWriteSuccessful = fwrite($this->sheetFilePointer, $rowXML); - if ($wasWriteSuccessful === false) { - throw new IOException("Unable to write data in {$this->worksheetFilePath}"); - } - } - - /** - * Build and return xml for a single cell. - * - * @param int $rowIndex - * @param int $cellNumber - * @param mixed $cellValue - * @param int $styleId - * @return string - * @throws InvalidArgumentException If the given value cannot be processed - */ - private function getCellXML($rowIndex, $cellNumber, $cellValue, $styleId) - { - $columnIndex = CellHelper::getCellIndexFromColumnIndex($cellNumber); - $cellXML = 'getCellXMLFragmentForNonEmptyString($cellValue); - } else if (CellHelper::isBoolean($cellValue)) { - $cellXML .= ' t="b">' . intval($cellValue) . ''; - } else if (CellHelper::isNumeric($cellValue)) { - $cellXML .= '>' . $cellValue . ''; - } else if (empty($cellValue)) { - if ($this->styleHelper->shouldApplyStyleOnEmptyCell($styleId)) { - $cellXML .= '/>'; - } else { - // don't write empty cells that do no need styling - // NOTE: not appending to $cellXML is the right behavior!! - $cellXML = ''; - } - } else { - throw new InvalidArgumentException('Trying to add a value with an unsupported type: ' . gettype($cellValue)); - } - - return $cellXML; - } - - /** - * Returns the XML fragment for a cell containing a non empty string - * - * @param string $cellValue The cell value - * @return string The XML fragment representing the cell - * @throws InvalidArgumentException If the string exceeds the maximum number of characters allowed per cell - */ - private function getCellXMLFragmentForNonEmptyString($cellValue) - { - if ($this->stringHelper->getStringLength($cellValue) > self::MAX_CHARACTERS_PER_CELL) { - throw new InvalidArgumentException('Trying to add a value that exceeds the maximum number of characters allowed in a cell (32,767)'); - } - - if ($this->shouldUseInlineStrings) { - $cellXMLFragment = ' t="inlineStr">' . $this->stringsEscaper->escape($cellValue) . ''; - } else { - $sharedStringId = $this->sharedStringsHelper->writeString($cellValue); - $cellXMLFragment = ' t="s">' . $sharedStringId . ''; - } - - return $cellXMLFragment; - } - - /** - * Closes the worksheet - * - * @return void - */ - public function close() - { - if (!is_resource($this->sheetFilePointer)) { - return; - } - - fwrite($this->sheetFilePointer, ''); - fwrite($this->sheetFilePointer, ''); - fclose($this->sheetFilePointer); - } -} diff --git a/src/Spout/Writer/XLSX/Writer.php b/src/Spout/Writer/XLSX/Writer.php deleted file mode 100644 index 965955a..0000000 --- a/src/Spout/Writer/XLSX/Writer.php +++ /dev/null @@ -1,132 +0,0 @@ -throwIfWriterAlreadyOpened('Writer must be configured before opening it.'); - - $this->tempFolder = $tempFolder; - return $this; - } - - /** - * Use inline string to be more memory efficient. If set to false, it will use shared strings. - * This must be set before opening the writer. - * - * @api - * @param bool $shouldUseInlineStrings Whether inline or shared strings should be used - * @return Writer - * @throws \Box\Spout\Writer\Exception\WriterAlreadyOpenedException If the writer was already opened - */ - public function setShouldUseInlineStrings($shouldUseInlineStrings) - { - $this->throwIfWriterAlreadyOpened('Writer must be configured before opening it.'); - - $this->shouldUseInlineStrings = $shouldUseInlineStrings; - return $this; - } - - /** - * Configures the write and sets the current sheet pointer to a new sheet. - * - * @return void - * @throws \Box\Spout\Common\Exception\IOException If unable to open the file for writing - */ - protected function openWriter() - { - if (!$this->book) { - $tempFolder = ($this->tempFolder) ? : sys_get_temp_dir(); - $this->book = new Workbook($tempFolder, $this->shouldUseInlineStrings, $this->shouldCreateNewSheetsAutomatically, $this->defaultRowStyle); - $this->book->addNewSheetAndMakeItCurrent(); - } - } - - /** - * @return Internal\Workbook The workbook representing the file to be written - */ - protected function getWorkbook() - { - return $this->book; - } - - /** - * Adds data to the currently opened writer. - * If shouldCreateNewSheetsAutomatically option is set to true, it will handle pagination - * with the creation of new worksheets if one worksheet has reached its maximum capicity. - * - * @param array $dataRow Array containing data to be written. - * Example $dataRow = ['data1', 1234, null, '', 'data5']; - * @param \Box\Spout\Writer\Style\Style $style Style to be applied to the row. - * @return void - * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the book is not created yet - * @throws \Box\Spout\Common\Exception\IOException If unable to write data - */ - protected function addRowToWriter(array $dataRow, $style) - { - $this->throwIfBookIsNotAvailable(); - $this->book->addRowToCurrentWorksheet($dataRow, $style); - } - - /** - * Returns the default style to be applied to rows. - * - * @return \Box\Spout\Writer\Style\Style - */ - protected function getDefaultRowStyle() - { - return (new StyleBuilder()) - ->setFontSize(self::DEFAULT_FONT_SIZE) - ->setFontName(self::DEFAULT_FONT_NAME) - ->build(); - } - - /** - * Closes the writer, preventing any additional writing. - * - * @return void - */ - protected function closeWriter() - { - if ($this->book) { - $this->book->close($this->filePointer); - } - } -} diff --git a/tests/Spout/Common/Escaper/ODSTest.php b/tests/Spout/Common/Escaper/ODSTest.php deleted file mode 100644 index 77b4913..0000000 --- a/tests/Spout/Common/Escaper/ODSTest.php +++ /dev/null @@ -1,42 +0,0 @@ -escape($stringToEscape); - - $this->assertEquals($expectedEscapedString, $escapedString, 'Incorrect escaped string'); - } -} diff --git a/tests/Spout/Common/Escaper/XLSXTest.php b/tests/Spout/Common/Escaper/XLSXTest.php deleted file mode 100644 index 0f2098f..0000000 --- a/tests/Spout/Common/Escaper/XLSXTest.php +++ /dev/null @@ -1,83 +0,0 @@ -escape($stringToEscape); - - $this->assertEquals($expectedEscapedString, $escapedString, 'Incorrect escaped string'); - } - - /** - * @return array - */ - public function dataProviderForTestUnescape() - { - return [ - ['test', 'test'], - ['adam's "car"', 'adam's "car"'], - ["\n", "\n"], - ["\r", "\r"], - ["\t", "\t"], - ['_x0000_', chr(0)], - ['_x0004_', chr(4)], - ['_x005F_x0000_', '_x0000_'], - ['_x0015_', chr(21)], - ['control _x0015_ character', 'control '.chr(21).' character'], - ['control's _x0015_ "character"', 'control's '.chr(21).' "character"'], - ]; - } - - /** - * @dataProvider dataProviderForTestUnescape - * - * @param string $stringToUnescape - * @param string $expectedUnescapedString - * @return void - */ - public function testUnescape($stringToUnescape, $expectedUnescapedString) - { - /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ - $escaper = \Box\Spout\Common\Escaper\XLSX::getInstance(); - $unescapedString = $escaper->unescape($stringToUnescape); - - $this->assertEquals($expectedUnescapedString, $unescapedString, 'Incorrect escaped string'); - } -} diff --git a/tests/Spout/Common/Helper/EncodingHelperTest.php b/tests/Spout/Common/Helper/EncodingHelperTest.php deleted file mode 100644 index d142d4e..0000000 --- a/tests/Spout/Common/Helper/EncodingHelperTest.php +++ /dev/null @@ -1,223 +0,0 @@ -getResourcePath($fileName); - $filePointer = fopen($resourcePath, 'r'); - - $encodingHelper = new EncodingHelper(new GlobalFunctionsHelper()); - $bytesOffset = $encodingHelper->getBytesOffsetToSkipBOM($filePointer, $encoding); - - $this->assertEquals($expectedBytesOffset, $bytesOffset); - } - - /** - * @return array - */ - public function dataProviderForIconvOrMbstringUsage() - { - return [ - [$shouldUseIconv = true], - [$shouldNotUseIconv = false], - ]; - } - - /** - * @dataProvider dataProviderForIconvOrMbstringUsage - * @expectedException \Box\Spout\Common\Exception\EncodingConversionException - * - * @param bool $shouldUseIconv - * @return void - */ - public function testAttemptConversionToUTF8ShouldThrowIfConversionFailed($shouldUseIconv) - { - $helperStub = $this->getMockBuilder('\Box\Spout\Common\Helper\GlobalFunctionsHelper') - ->setMethods(['iconv', 'mb_convert_encoding']) - ->getMock(); - $helperStub->method('iconv')->willReturn(false); - $helperStub->method('mb_convert_encoding')->willReturn(false); - - /** @var EncodingHelper $encodingHelperStub */ - $encodingHelperStub = $this->getMockBuilder('\Box\Spout\Common\Helper\EncodingHelper') - ->setConstructorArgs([$helperStub]) - ->setMethods(['canUseIconv', 'canUseMbString']) - ->getMock(); - $encodingHelperStub->method('canUseIconv')->willReturn($shouldUseIconv); - $encodingHelperStub->method('canUseMbString')->willReturn(true); - - $encodingHelperStub->attemptConversionToUTF8('input', EncodingHelper::ENCODING_UTF16_LE); - } - - /** - * @expectedException \Box\Spout\Common\Exception\EncodingConversionException - * - * @return void - */ - public function testAttemptConversionToUTF8ShouldThrowIfConversionNotSupported() - { - /** @var EncodingHelper $encodingHelperStub */ - $encodingHelperStub = $this->getMockBuilder('\Box\Spout\Common\Helper\EncodingHelper') - ->disableOriginalConstructor() - ->setMethods(['canUseIconv', 'canUseMbString']) - ->getMock(); - $encodingHelperStub->method('canUseIconv')->willReturn(false); - $encodingHelperStub->method('canUseMbString')->willReturn(false); - - $encodingHelperStub->attemptConversionToUTF8('input', EncodingHelper::ENCODING_UTF16_LE); - } - - /** - * @dataProvider dataProviderForIconvOrMbstringUsage - * - * @param bool $shouldUseIconv - * @return void - */ - public function testAttemptConversionToUTF8ShouldReturnReencodedString($shouldUseIconv) - { - /** @var EncodingHelper $encodingHelperStub */ - $encodingHelperStub = $this->getMockBuilder('\Box\Spout\Common\Helper\EncodingHelper') - ->setConstructorArgs([new GlobalFunctionsHelper()]) - ->setMethods(['canUseIconv', 'canUseMbString']) - ->getMock(); - $encodingHelperStub->method('canUseIconv')->willReturn($shouldUseIconv); - $encodingHelperStub->method('canUseMbString')->willReturn(true); - - $encodedString = iconv(EncodingHelper::ENCODING_UTF8, EncodingHelper::ENCODING_UTF16_LE, 'input'); - $decodedString = $encodingHelperStub->attemptConversionToUTF8($encodedString, EncodingHelper::ENCODING_UTF16_LE); - - $this->assertEquals('input', $decodedString); - } - - /** - * @return void - */ - public function testAttemptConversionToUTF8ShouldBeNoopWhenTargetIsUTF8() - { - /** @var EncodingHelper $encodingHelperStub */ - $encodingHelperStub = $this->getMockBuilder('\Box\Spout\Common\Helper\EncodingHelper') - ->disableOriginalConstructor() - ->setMethods(['canUseIconv']) - ->getMock(); - $encodingHelperStub->expects($this->never())->method('canUseIconv'); - - $decodedString = $encodingHelperStub->attemptConversionToUTF8('input', EncodingHelper::ENCODING_UTF8); - $this->assertEquals('input', $decodedString); - } - - /** - * @dataProvider dataProviderForIconvOrMbstringUsage - * @expectedException \Box\Spout\Common\Exception\EncodingConversionException - * - * @param bool $shouldUseIconv - * @return void - */ - public function testAttemptConversionFromUTF8ShouldThrowIfConversionFailed($shouldUseIconv) - { - $helperStub = $this->getMockBuilder('\Box\Spout\Common\Helper\GlobalFunctionsHelper') - ->setMethods(['iconv', 'mb_convert_encoding']) - ->getMock(); - $helperStub->method('iconv')->willReturn(false); - $helperStub->method('mb_convert_encoding')->willReturn(false); - - /** @var EncodingHelper $encodingHelperStub */ - $encodingHelperStub = $this->getMockBuilder('\Box\Spout\Common\Helper\EncodingHelper') - ->setConstructorArgs([$helperStub]) - ->setMethods(['canUseIconv', 'canUseMbString']) - ->getMock(); - $encodingHelperStub->method('canUseIconv')->willReturn($shouldUseIconv); - $encodingHelperStub->method('canUseMbString')->willReturn(true); - - $encodingHelperStub->attemptConversionFromUTF8('input', EncodingHelper::ENCODING_UTF16_LE); - } - - /** - * @expectedException \Box\Spout\Common\Exception\EncodingConversionException - * - * @return void - */ - public function testAttemptConversionFromUTF8ShouldThrowIfConversionNotSupported() - { - /** @var EncodingHelper $encodingHelperStub */ - $encodingHelperStub = $this->getMockBuilder('\Box\Spout\Common\Helper\EncodingHelper') - ->disableOriginalConstructor() - ->setMethods(['canUseIconv', 'canUseMbString']) - ->getMock(); - $encodingHelperStub->method('canUseIconv')->willReturn(false); - $encodingHelperStub->method('canUseMbString')->willReturn(false); - - $encodingHelperStub->attemptConversionFromUTF8('input', EncodingHelper::ENCODING_UTF16_LE); - } - - /** - * @dataProvider dataProviderForIconvOrMbstringUsage - * - * @param bool $shouldUseIconv - * @return void - */ - public function testAttemptConversionFromUTF8ShouldReturnReencodedString($shouldUseIconv) - { - /** @var EncodingHelper $encodingHelperStub */ - $encodingHelperStub = $this->getMockBuilder('\Box\Spout\Common\Helper\EncodingHelper') - ->setConstructorArgs([new GlobalFunctionsHelper()]) - ->setMethods(['canUseIconv', 'canUseMbString']) - ->getMock(); - $encodingHelperStub->method('canUseIconv')->willReturn($shouldUseIconv); - $encodingHelperStub->method('canUseMbString')->willReturn(true); - - $encodedString = $encodingHelperStub->attemptConversionFromUTF8('input', EncodingHelper::ENCODING_UTF16_LE); - $encodedStringWithIconv = iconv(EncodingHelper::ENCODING_UTF8, EncodingHelper::ENCODING_UTF16_LE, 'input'); - - $this->assertEquals($encodedStringWithIconv, $encodedString); - } - - /** - * @return void - */ - public function testAttemptConversionFromUTF8ShouldBeNoopWhenTargetIsUTF8() - { - /** @var EncodingHelper $encodingHelperStub */ - $encodingHelperStub = $this->getMockBuilder('\Box\Spout\Common\Helper\EncodingHelper') - ->disableOriginalConstructor() - ->setMethods(['canUseIconv']) - ->getMock(); - $encodingHelperStub->expects($this->never())->method('canUseIconv'); - - $encodedString = $encodingHelperStub->attemptConversionFromUTF8('input', EncodingHelper::ENCODING_UTF8); - $this->assertEquals('input', $encodedString); - } -} diff --git a/tests/Spout/Common/Helper/FileSystemHelperTest.php b/tests/Spout/Common/Helper/FileSystemHelperTest.php deleted file mode 100644 index a73e7f2..0000000 --- a/tests/Spout/Common/Helper/FileSystemHelperTest.php +++ /dev/null @@ -1,59 +0,0 @@ -fileSystemHelper = new FileSystemHelper($baseFolder); - } - - /** - * @expectedException \Box\Spout\Common\Exception\IOException - * @return void - */ - public function testCreateFolderShouldThrowExceptionIfOutsideOfBaseFolder() - { - $this->fileSystemHelper->createFolder('/tmp/folder_outside_base_folder', 'folder_name'); - } - - /** - * @expectedException \Box\Spout\Common\Exception\IOException - * @return void - */ - public function testCreateFileWithContentsShouldThrowExceptionIfOutsideOfBaseFolder() - { - $this->fileSystemHelper->createFileWithContents('/tmp/folder_outside_base_folder', 'file_name', 'contents'); - } - - /** - * @expectedException \Box\Spout\Common\Exception\IOException - * @return void - */ - public function testDeleteFileShouldThrowExceptionIfOutsideOfBaseFolder() - { - $this->fileSystemHelper->deleteFile('/tmp/folder_outside_base_folder/file_name'); - } - - /** - * @expectedException \Box\Spout\Common\Exception\IOException - * @return void - */ - public function testDeleteFolderRecursivelyShouldThrowExceptionIfOutsideOfBaseFolder() - { - $this->fileSystemHelper->deleteFolderRecursively('/tmp/folder_outside_base_folder'); - } -} diff --git a/tests/Spout/Reader/CSV/ReaderTest.php b/tests/Spout/Reader/CSV/ReaderTest.php deleted file mode 100644 index 429ffa6..0000000 --- a/tests/Spout/Reader/CSV/ReaderTest.php +++ /dev/null @@ -1,518 +0,0 @@ -open('/path/to/fake/file.csv'); - } - - /** - * @expectedException \Box\Spout\Reader\Exception\ReaderNotOpenedException - * - * @return void - */ - public function testOpenShouldThrowExceptionIfTryingToReadBeforeOpeningReader() - { - ReaderFactory::create(Type::CSV)->getSheetIterator(); - } - - /** - * @expectedException \Box\Spout\Common\Exception\IOException - * - * @return void - */ - public function testOpenShouldThrowExceptionIfFileNotReadable() - { - $helperStub = $this->getMockBuilder('\Box\Spout\Common\Helper\GlobalFunctionsHelper') - ->setMethods(['is_readable']) - ->getMock(); - $helperStub->method('is_readable')->willReturn(false); - - $resourcePath = $this->getResourcePath('csv_standard.csv'); - - $reader = ReaderFactory::create(Type::CSV); - $reader->setGlobalFunctionsHelper($helperStub); - $reader->open($resourcePath); - } - - /** - * @expectedException \Box\Spout\Common\Exception\IOException - * - * @return void - */ - public function testOpenShouldThrowExceptionIfCannotOpenFile() - { - $helperStub = $this->getMockBuilder('\Box\Spout\Common\Helper\GlobalFunctionsHelper') - ->setMethods(['fopen']) - ->getMock(); - $helperStub->method('fopen')->willReturn(false); - - $resourcePath = $this->getResourcePath('csv_standard.csv'); - - $reader = ReaderFactory::create(Type::CSV); - $reader->setGlobalFunctionsHelper($helperStub); - $reader->open($resourcePath); - } - - - /** - * @return void - */ - public function testReadStandardCSV() - { - $allRows = $this->getAllRowsForFile('csv_standard.csv'); - - $expectedRows = [ - ['csv--11', 'csv--12', 'csv--13'], - ['csv--21', 'csv--22', 'csv--23'], - ['csv--31', 'csv--32', 'csv--33'], - ]; - $this->assertEquals($expectedRows, $allRows); - } - - /** - * @return void - */ - public function testReadShouldNotStopAtCommaIfEnclosed() - { - $allRows = $this->getAllRowsForFile('csv_with_comma_enclosed.csv'); - $this->assertEquals('This is, a comma', $allRows[0][0]); - } - - /** - * @return void - */ - public function testReadShouldKeepEmptyCells() - { - $allRows = $this->getAllRowsForFile('csv_with_empty_cells.csv'); - - $expectedRows = [ - ['csv--11', 'csv--12', 'csv--13'], - ['csv--21', '', 'csv--23'], - ['csv--31', 'csv--32', ''], - ]; - $this->assertEquals($expectedRows, $allRows); - } - - /** - * @return void - */ - public function testReadShouldSkipEmptyLinesIfShouldPreserveEmptyRowsNotSet() - { - $allRows = $this->getAllRowsForFile('csv_with_multiple_empty_lines.csv'); - - $expectedRows = [ - // skipped row here - ['csv--21', 'csv--22', 'csv--23'], - // skipped row here - ['csv--41', 'csv--42', 'csv--43'], - // skipped row here - // last row empty - ]; - $this->assertEquals($expectedRows, $allRows); - } - - /** - * @return void - */ - public function testReadShouldReturnEmptyLinesIfShouldPreserveEmptyRowsSet() - { - $allRows = $this->getAllRowsForFile( - 'csv_with_multiple_empty_lines.csv', - ',', '"', "\n", EncodingHelper::ENCODING_UTF8, - $shouldPreserveEmptyRows = true - ); - - $expectedRows = [ - [''], - ['csv--21', 'csv--22', 'csv--23'], - [''], - ['csv--41', 'csv--42', 'csv--43'], - [''], - ]; - $this->assertEquals($expectedRows, $allRows); - } - - /** - * @return array - */ - public function dataProviderForTestReadShouldReadEmptyFile() - { - return [ - ['csv_empty.csv'], - ['csv_all_lines_empty.csv'], - ]; - } - - /** - * @dataProvider dataProviderForTestReadShouldReadEmptyFile - * - * @param string $fileName - * @return void - */ - public function testReadShouldReadEmptyFile($fileName) - { - $allRows = $this->getAllRowsForFile($fileName); - $this->assertEquals([], $allRows); - } - - /** - * @return void - */ - public function testReadShouldHaveTheRightNumberOfCells() - { - $allRows = $this->getAllRowsForFile('csv_with_different_cells_number.csv'); - - $expectedRows = [ - ['csv--11', 'csv--12', 'csv--13'], - ['csv--21', 'csv--22'], - ['csv--31'], - ]; - $this->assertEquals($expectedRows, $allRows); - } - - /** - * @return void - */ - public function testReadShouldSupportCustomFieldDelimiter() - { - $allRows = $this->getAllRowsForFile('csv_delimited_with_pipes.csv', '|'); - - $expectedRows = [ - ['csv--11', 'csv--12', 'csv--13'], - ['csv--21', 'csv--22', 'csv--23'], - ['csv--31', 'csv--32', 'csv--33'], - ]; - $this->assertEquals($expectedRows, $allRows); - } - - /** - * @return void - */ - public function testReadShouldSupportCustomFieldEnclosure() - { - $allRows = $this->getAllRowsForFile('csv_text_enclosed_with_pound.csv', ',', '#'); - $this->assertEquals('This is, a comma', $allRows[0][0]); - } - - /** - * @return void - */ - public function testReadCustomEOLs() - { - $allRows = $this->getAllRowsForFile('csv_with_CR_EOL.csv', ',', '"', "\r"); - - $expectedRows = [ - ['csv--11', 'csv--12', 'csv--13'], - ['csv--21', 'csv--22', 'csv--23'], - ['csv--31', 'csv--32', 'csv--33'], - ]; - $this->assertEquals($expectedRows, $allRows); - } - - /** - * @return void - */ - public function testReadShouldNotTruncateLineBreak() - { - $allRows = $this->getAllRowsForFile('csv_with_line_breaks.csv', ','); - $this->assertEquals("This is,\na comma", $allRows[0][0]); - } - - /** - * @return array - */ - public function dataProviderForTestReadShouldSkipBom() - { - return [ - ['csv_with_utf8_bom.csv', EncodingHelper::ENCODING_UTF8], - ['csv_with_utf16le_bom.csv', EncodingHelper::ENCODING_UTF16_LE], - ['csv_with_utf16be_bom.csv', EncodingHelper::ENCODING_UTF16_BE], - ['csv_with_utf32le_bom.csv', EncodingHelper::ENCODING_UTF32_LE], - ['csv_with_utf32be_bom.csv', EncodingHelper::ENCODING_UTF32_BE], - ]; - } - - /** - * @dataProvider dataProviderForTestReadShouldSkipBom - * - * @param string $fileName - * @param string $fileEncoding - * @return void - */ - public function testReadShouldSkipBom($fileName, $fileEncoding) - { - $allRows = $this->getAllRowsForFile($fileName, ',', '"', "\n", $fileEncoding); - - $expectedRows = [ - ['csv--11', 'csv--12', 'csv--13'], - ['csv--21', 'csv--22', 'csv--23'], - ['csv--31', 'csv--32', 'csv--33'], - ]; - $this->assertEquals($expectedRows, $allRows); - } - - /** - * @return array - */ - public function dataProviderForTestReadShouldSupportNonUTF8FilesWithoutBOMs() - { - $shouldUseIconv = true; - $shouldNotUseIconv = false; - - return [ - ['csv_with_encoding_utf16le_no_bom.csv', EncodingHelper::ENCODING_UTF16_LE, $shouldUseIconv], - ['csv_with_encoding_utf16le_no_bom.csv', EncodingHelper::ENCODING_UTF16_LE, $shouldNotUseIconv], - ['csv_with_encoding_cp1252.csv', 'CP1252', $shouldUseIconv], - ['csv_with_encoding_cp1252.csv', 'CP1252', $shouldNotUseIconv], - ]; - } - - /** - * @dataProvider dataProviderForTestReadShouldSupportNonUTF8FilesWithoutBOMs - * - * @param string $fileName - * @param string $fileEncoding - * @param bool $shouldUseIconv - * @return void - */ - public function testReadShouldSupportNonUTF8FilesWithoutBOMs($fileName, $fileEncoding, $shouldUseIconv) - { - $allRows = []; - $resourcePath = $this->getResourcePath($fileName); - - /** @var \Box\Spout\Common\Helper\GlobalFunctionsHelper|\PHPUnit_Framework_MockObject_MockObject $helperStub */ - $helperStub = $this->getMockBuilder('\Box\Spout\Common\Helper\GlobalFunctionsHelper') - ->setMethods(['function_exists']) - ->getMock(); - - $returnValueMap = [ - ['iconv', $shouldUseIconv], - ['mb_convert_encoding', true], - ]; - $helperStub->method('function_exists')->will($this->returnValueMap($returnValueMap)); - - /** @var \Box\Spout\Reader\CSV\Reader $reader */ - $reader = ReaderFactory::create(Type::CSV); - $reader - ->setGlobalFunctionsHelper($helperStub) - ->setEncoding($fileEncoding) - ->open($resourcePath); - - foreach ($reader->getSheetIterator() as $sheet) { - foreach ($sheet->getRowIterator() as $row) { - $allRows[] = $row; - } - } - - $reader->close(); - - $expectedRows = [ - ['csv--11', 'csv--12', 'csv--13'], - ['csv--21', 'csv--22', 'csv--23'], - ['csv--31', 'csv--32', 'csv--33'], - ]; - $this->assertEquals($expectedRows, $allRows); - } - - /** - * @return void - */ - public function testReadMultipleTimesShouldRewindReader() - { - $allRows = []; - $resourcePath = $this->getResourcePath('csv_standard.csv'); - - $reader = ReaderFactory::create(Type::CSV); - $reader->open($resourcePath); - - foreach ($reader->getSheetIterator() as $sheet) { - // do nothing - } - - foreach ($reader->getSheetIterator() as $sheet) { - foreach ($sheet->getRowIterator() as $row) { - $allRows[] = $row; - break; - } - - foreach ($sheet->getRowIterator() as $row) { - $allRows[] = $row; - break; - } - } - - foreach ($reader->getSheetIterator() as $sheet) { - foreach ($sheet->getRowIterator() as $row) { - $allRows[] = $row; - break; - } - } - - $reader->close(); - - $expectedRows = [ - ['csv--11', 'csv--12', 'csv--13'], - ['csv--11', 'csv--12', 'csv--13'], - ['csv--11', 'csv--12', 'csv--13'], - ]; - $this->assertEquals($expectedRows, $allRows); - } - - /** - * https://github.com/box/spout/issues/184 - * @return void - */ - public function testReadShouldInludeRowsWithZerosOnly() - { - $allRows = $this->getAllRowsForFile('sheet_with_zeros_in_row.csv'); - - $expectedRows = [ - ['A', 'B', 'C'], - ['1', '2', '3'], - ['0', '0', '0'] - ]; - $this->assertEquals($expectedRows, $allRows, 'There should be only 3 rows, because zeros (0) are valid values'); - } - - /** - * https://github.com/box/spout/issues/184 - * @return void - */ - public function testReadShouldCreateOutputEmptyCellPreserved() - { - $allRows = $this->getAllRowsForFile('sheet_with_empty_cells.csv'); - - $expectedRows = [ - ['A', 'B', 'C'], - ['0', '', ''], - ['1', '1', ''] - ]; - $this->assertEquals($expectedRows, $allRows, 'There should be 3 rows, with equal length'); - } - - /** - * https://github.com/box/spout/issues/195 - * @return void - */ - public function testReaderShouldNotTrimCellValues() - { - $allRows = $this->getAllRowsForFile('sheet_with_untrimmed_strings.csv'); - - $expectedRows = [ - ['A'], - [' A '], - ["\n\tA\n\t"], - ]; - - $this->assertEquals($expectedRows, $allRows, 'Cell values should not be trimmed'); - } - - /** - * @param string $fileName - * @param string|void $fieldDelimiter - * @param string|void $fieldEnclosure - * @param string|void $endOfLineCharacter - * @param string|void $encoding - * @param bool|void $shouldPreserveEmptyRows - * @return array All the read rows the given file - */ - private function getAllRowsForFile( - $fileName, - $fieldDelimiter = ',', - $fieldEnclosure = '"', - $endOfLineCharacter = "\n", - $encoding = EncodingHelper::ENCODING_UTF8, - $shouldPreserveEmptyRows = false) - { - $allRows = []; - $resourcePath = $this->getResourcePath($fileName); - - /** @var \Box\Spout\Reader\CSV\Reader $reader */ - $reader = ReaderFactory::create(Type::CSV); - $reader - ->setFieldDelimiter($fieldDelimiter) - ->setFieldEnclosure($fieldEnclosure) - ->setEndOfLineCharacter($endOfLineCharacter) - ->setEncoding($encoding) - ->setShouldPreserveEmptyRows($shouldPreserveEmptyRows) - ->open($resourcePath); - - foreach ($reader->getSheetIterator() as $sheetIndex => $sheet) { - foreach ($sheet->getRowIterator() as $rowIndex => $row) { - $allRows[] = $row; - } - } - - $reader->close(); - - return $allRows; - } - - /** - * @return void - */ - public function testReadCustomStreamWrapper() - { - $allRows = []; - $resourcePath = 'spout://csv_standard'; - - // register stream wrapper - stream_wrapper_register('spout', SpoutTestStream::CLASS_NAME); - - /** @var \Box\Spout\Reader\CSV\Reader $reader */ - $reader = ReaderFactory::create(Type::CSV); - $reader->open($resourcePath); - - foreach ($reader->getSheetIterator() as $sheet) { - foreach ($sheet->getRowIterator() as $row) { - $allRows[] = $row; - } - } - - $reader->close(); - - $expectedRows = [ - ['csv--11', 'csv--12', 'csv--13'], - ['csv--21', 'csv--22', 'csv--23'], - ['csv--31', 'csv--32', 'csv--33'], - ]; - $this->assertEquals($expectedRows, $allRows); - - // cleanup - stream_wrapper_unregister('spout'); - } - - /** - * @expectedException \Box\Spout\Common\Exception\IOException - * - * @return void - */ - public function testReadWithUnsupportedCustomStreamWrapper() - { - /** @var \Box\Spout\Reader\CSV\Reader $reader */ - $reader = ReaderFactory::create(Type::CSV); - $reader->open('unsupported://foobar'); - } - -} diff --git a/tests/Spout/Reader/CSV/SheetTest.php b/tests/Spout/Reader/CSV/SheetTest.php deleted file mode 100644 index ae18c2f..0000000 --- a/tests/Spout/Reader/CSV/SheetTest.php +++ /dev/null @@ -1,46 +0,0 @@ -openFileAndReturnSheet('csv_standard.csv'); - - $this->assertEquals('', $sheet->getName()); - $this->assertEquals(0, $sheet->getIndex()); - $this->assertTrue($sheet->isActive()); - } - - /** - * @param string $fileName - * @return Sheet - */ - private function openFileAndReturnSheet($fileName) - { - $resourcePath = $this->getResourcePath($fileName); - $reader = ReaderFactory::create(Type::CSV); - $reader->open($resourcePath); - - $sheet = $reader->getSheetIterator()->current(); - - $reader->close(); - - return $sheet; - } -} diff --git a/tests/Spout/Reader/CSV/SpoutTestStream.php b/tests/Spout/Reader/CSV/SpoutTestStream.php deleted file mode 100644 index fdaeb18..0000000 --- a/tests/Spout/Reader/CSV/SpoutTestStream.php +++ /dev/null @@ -1,120 +0,0 @@ -getFilePathFromStreamPath($path); - return stat($filePath); - } - - /** - * @param string $streamPath - * @return string - */ - private function getFilePathFromStreamPath($streamPath) - { - $fileName = parse_url($streamPath, PHP_URL_HOST); - return self::PATH_TO_CSV_RESOURCES . $fileName . self::CSV_EXTENSION; - } - - /** - * @param string $path - * @param string $mode - * @param int $options - * @param string $opened_path - * @return bool - */ - public function stream_open($path, $mode, $options, &$opened_path) - { - $this->position = 0; - - // the path is like "spout://csv_name" so the actual file name correspond the name of the host. - $filePath = $this->getFilePathFromStreamPath($path); - $this->fileHandle = fopen($filePath, $mode); - - return true; - } - - /** - * @param int $numBytes - * @return string - */ - public function stream_read($numBytes) - { - $this->position += $numBytes; - return fread($this->fileHandle, $numBytes); - } - - /** - * @return int - */ - public function stream_tell() - { - return $this->position; - } - - /** - * @param int $offset - * @param int|void $whence - * @return bool - */ - public function stream_seek($offset, $whence = SEEK_SET) - { - $result = fseek($this->fileHandle, $offset, $whence); - if ($result === -1) { - return false; - } - - if ($whence === SEEK_SET) { - $this->position = $offset; - } else if ($whence === SEEK_CUR) { - $this->position += $offset; - } else { - // not implemented - } - - return true; - } - - /** - * @return bool - */ - public function stream_close() - { - return fclose($this->fileHandle); - } - - /** - * @return bool - */ - public function stream_eof() - { - return feof($this->fileHandle); - } -} diff --git a/tests/Spout/Reader/ODS/ReaderTest.php b/tests/Spout/Reader/ODS/ReaderTest.php deleted file mode 100644 index f36348f..0000000 --- a/tests/Spout/Reader/ODS/ReaderTest.php +++ /dev/null @@ -1,549 +0,0 @@ -getAllRowsForFile($filePath); - } - - /** - * @return array - */ - public function dataProviderForTestReadForAllWorksheets() - { - return [ - ['one_sheet_with_strings.ods', 2, 3], - ['two_sheets_with_strings.ods', 4, 3], - ]; - } - - /** - * @dataProvider dataProviderForTestReadForAllWorksheets - * - * @param string $resourceName - * @param int $expectedNumOfRows - * @param int $expectedNumOfCellsPerRow - * @return void - */ - public function testReadForAllWorksheets($resourceName, $expectedNumOfRows, $expectedNumOfCellsPerRow) - { - $allRows = $this->getAllRowsForFile($resourceName); - - $this->assertEquals($expectedNumOfRows, count($allRows), "There should be $expectedNumOfRows rows"); - foreach ($allRows as $row) { - $this->assertEquals($expectedNumOfCellsPerRow, count($row), "There should be $expectedNumOfCellsPerRow cells for every row"); - } - } - - /** - * @return void - */ - public function testReadShouldSupportRowWithOnlyOneCell() - { - $allRows = $this->getAllRowsForFile('sheet_with_only_one_cell.ods'); - $this->assertEquals([['foo']], $allRows); - } - - /** - * @return void - */ - public function testReadShouldSupportNumberRowsRepeated() - { - $allRows = $this->getAllRowsForFile('sheet_with_number_rows_repeated.ods'); - $expectedRows = [ - ['foo', 10.43], - ['foo', 10.43], - ]; - $this->assertEquals($expectedRows, $allRows); - } - - /** - * @return void - */ - public function testReadShouldSupportNumberColumnsRepeated() - { - $allRows = $this->getAllRowsForFile('sheet_with_number_columns_repeated.ods'); - $expectedRows = [ - [ - 'foo', 'foo', 'foo', - '', '', - true, true, - 10.43, 10.43, 10.43, 10.43, - ], - ]; - $this->assertEquals($expectedRows, $allRows); - } - - /** - * @return array - */ - public function dataProviderForTestReadWithFilesGeneratedByExternalSoftwares() - { - return [ - ['file_generated_by_libre_office.ods', true], - ['file_generated_by_excel_2010_windows.ods', false], - ['file_generated_by_excel_office_online.ods', false], - ]; - } - - /** - * @dataProvider dataProviderForTestReadWithFilesGeneratedByExternalSoftwares - * The files contain styles, different value types, gaps between cells, - * repeated values, empty row, different number of cells per row. - * - * @param bool $skipLastEmptyValues - * @param string $fileName - * @return void - */ - public function testReadWithFilesGeneratedByExternalSoftwares($fileName, $skipLastEmptyValues) - { - $allRows = $this->getAllRowsForFile($fileName); - - $expectedRows = [ - ['header1','header2','header3','header4'], - ['val11','val12','val13','val14'], - ['val21','','val23','val23'], - ['', 10.43, 29.11], - ]; - - // In the description of the last cell, Excel specifies that the empty value needs to be repeated - // a lot of times (16384 - number of cells used in the row). To avoid creating 16384 cells all the time, - // this cell is skipped alltogether. - if ($skipLastEmptyValues) { - $expectedRows[3][] = ''; - } - - $this->assertEquals($expectedRows, $allRows); - } - - - /** - * @return void - */ - public function testReadShouldSupportAllCellTypes() - { - $utcTz = new \DateTimeZone('UTC'); - $honoluluTz = new \DateTimeZone('Pacific/Honolulu'); // UTC-10 - - $allRows = $this->getAllRowsForFile('sheet_with_all_cell_types.ods'); - - $expectedRows = [ - [ - 'ods--11', 'ods--12', - true, false, - 0, 10.43, - new \DateTime('1987-11-29T00:00:00', $utcTz), new \DateTime('1987-11-29T13:37:00', $utcTz), - new \DateTime('1987-11-29T13:37:00', $utcTz), new \DateTime('1987-11-29T13:37:00', $honoluluTz), - new \DateInterval('PT13H37M00S'), - 0, 0.42, - '42 USD', '9.99 EUR', - '', - ], - ]; - $this->assertEquals($expectedRows, $allRows); - } - - /** - * @return void - */ - public function testReadShouldSupportFormatDatesAndTimesIfSpecified() - { - $shouldFormatDates = true; - $allRows = $this->getAllRowsForFile('sheet_with_dates_and_times.ods', $shouldFormatDates); - - $expectedRows = [ - ['05/19/2016', '5/19/16', '05/19/2016 16:39:00', '05/19/16 04:39 PM', '5/19/2016'], - ['11:29', '13:23:45', '01:23:45', '01:23:45 AM', '01:23:45 PM'], - ]; - $this->assertEquals($expectedRows, $allRows); - } - - /** - * @return void - */ - public function testReadShouldReturnEmptyStringOnUndefinedCellType() - { - $allRows = $this->getAllRowsForFile('sheet_with_undefined_value_type.ods'); - $this->assertEquals([['ods--11', '', 'ods--13']], $allRows); - } - - /** - * @return void - */ - public function testReadShouldReturnNullOnInvalidDateOrTime() - { - $allRows = $this->getAllRowsForFile('sheet_with_invalid_date_time.ods'); - $this->assertEquals([[null, null]], $allRows); - } - - /** - * @return void - */ - public function testReadShouldSupportMultilineStrings() - { - $allRows = $this->getAllRowsForFile('sheet_with_multiline_string.ods'); - - $expectedRows = [["string\non multiple\nlines!"]]; - $this->assertEquals($expectedRows, $allRows); - } - - /** - * @return void - */ - public function testReadShouldSkipEmptyRowsIfShouldPreserveEmptyRowsNotSet() - { - $allRows = $this->getAllRowsForFile('sheet_with_empty_rows.ods'); - - $this->assertEquals(3, count($allRows), 'There should be only 3 rows, because the empty rows are skipped'); - - $expectedRows = [ - // skipped row here - ['ods--21', 'ods--22', 'ods--23'], - // skipped row here - // skipped row here - ['ods--51', 'ods--52', 'ods--53'], - ['ods--61', 'ods--62', 'ods--63'], - ]; - $this->assertEquals($expectedRows, $allRows); - } - - /** - * @return void - */ - public function testReadShouldReturnEmptyLinesIfShouldPreserveEmptyRowsSet() - { - $allRows = $this->getAllRowsForFile('sheet_with_empty_rows.ods', false, true); - - $this->assertEquals(6, count($allRows), 'There should be 6 rows'); - - $expectedRows = [ - [''], - ['ods--21', 'ods--22', 'ods--23'], - [''], - [''], - ['ods--51', 'ods--52', 'ods--53'], - ['ods--61', 'ods--62', 'ods--63'], - ]; - $this->assertEquals($expectedRows, $allRows); - } - - /** - * @return void - */ - public function testReadShouldPreserveSpacing() - { - $allRows = $this->getAllRowsForFile('sheet_with_various_spaces.ods'); - - $expectedRow = [ - ' 4 spaces before and after ', - ' 1 space before and after ', - '2 spaces after ', - ' 2 spaces before', - "3 spaces in the middle\nand 2 spaces in the middle", - ]; - $this->assertEquals([$expectedRow], $allRows); - } - - - /** - * @NOTE: The LIBXML_NOENT is used to ACTUALLY substitute entities (and should therefore not be used) - * - * @return void - */ - public function testReadShouldBeProtectedAgainstBillionLaughsAttack() - { - $startTime = microtime(true); - $fileName = 'attack_billion_laughs.ods'; - - try { - // using @ to prevent warnings/errors from being displayed - @$this->getAllRowsForFile($fileName); - $this->fail('An exception should have been thrown'); - } catch (IOException $exception) { - $duration = microtime(true) - $startTime; - $this->assertLessThan(10, $duration, 'Entities should not be expanded and therefore take more than 10 seconds to be parsed.'); - - $expectedMaxMemoryUsage = 30 * 1024 * 1024; // 30MB - $this->assertLessThan($expectedMaxMemoryUsage, memory_get_peak_usage(true), 'Entities should not be expanded and therefore consume all the memory.'); - } - } - - /** - * @NOTE: The LIBXML_NOENT is used to ACTUALLY substitute entities (and should therefore not be used) - * - * @return void - */ - public function testReadShouldBeProtectedAgainstQuadraticBlowupAttack() - { - $startTime = microtime(true); - - $fileName = 'attack_quadratic_blowup.ods'; - $allRows = $this->getAllRowsForFile($fileName); - - $this->assertEquals('', $allRows[0][0], 'Entities should not have been expanded'); - - $duration = microtime(true) - $startTime; - $this->assertLessThan(10, $duration, 'Entities should not be expanded and therefore take more than 10 seconds to be parsed.'); - - $expectedMaxMemoryUsage = 30 * 1024 * 1024; // 30MB - $this->assertLessThan($expectedMaxMemoryUsage, memory_get_peak_usage(true), 'Entities should not be expanded and therefore consume all the memory.'); - } - - /** - * @return void - */ - public function testReadShouldBeAbleToProcessEmptySheets() - { - $allRows = $this->getAllRowsForFile('sheet_with_no_cells.ods'); - $this->assertEquals([], $allRows, 'Sheet with no cells should be correctly processed.'); - } - - /** - * @return void - */ - public function testReadShouldSkipFormulas() - { - $allRows = $this->getAllRowsForFile('sheet_with_formulas.ods'); - - $expectedRows = [ - ['val1', 'val2', 'total1', 'total2'], - [10, 20, 30, 21], - [11, 21, 32, 41], - ]; - $this->assertEquals($expectedRows, $allRows); - } - - /** - * @expectedException \Box\Spout\Reader\Exception\IteratorNotRewindableException - * - * @return void - */ - public function testReadShouldThrowIfTryingToRewindRowIterator() - { - $resourcePath = $this->getResourcePath('one_sheet_with_strings.ods'); - $reader = ReaderFactory::create(Type::ODS); - $reader->open($resourcePath); - - foreach ($reader->getSheetIterator() as $sheet) { - // start looping throw the rows - foreach ($sheet->getRowIterator() as $row) { - break; - } - - // this will rewind the row iterator - foreach ($sheet->getRowIterator() as $row) { - break; - } - } - } - - /** - * @return void - */ - public function testReadMultipleTimesShouldRewindReader() - { - $allRows = []; - $resourcePath = $this->getResourcePath('two_sheets_with_strings.ods'); - - $reader = ReaderFactory::create(Type::ODS); - $reader->open($resourcePath); - - foreach ($reader->getSheetIterator() as $sheet) { - // do nothing - } - - // this loop should only add the first row of each sheet - foreach ($reader->getSheetIterator() as $sheet) { - foreach ($sheet->getRowIterator() as $row) { - $allRows[] = $row; - break; - } - } - - // this loop should only add the first row of the first sheet - foreach ($reader->getSheetIterator() as $sheet) { - foreach ($sheet->getRowIterator() as $row) { - $allRows[] = $row; - break; - } - - // stop reading more sheets - break; - } - - $reader->close(); - - $expectedRows = [ - ['ods--sheet1--11', 'ods--sheet1--12', 'ods--sheet1--13'], // 1st row, 1st sheet - ['ods--sheet2--11', 'ods--sheet2--12', 'ods--sheet2--13'], // 1st row, 2nd sheet - ['ods--sheet1--11', 'ods--sheet1--12', 'ods--sheet1--13'], // 1st row, 1st sheet - ]; - $this->assertEquals($expectedRows, $allRows); - } - - /** - * @expectedException \Box\Spout\Common\Exception\IOException - * - * @return void - */ - public function testReadWithUnsupportedCustomStreamWrapper() - { - /** @var \Box\Spout\Reader\ODS\Reader $reader */ - $reader = ReaderFactory::create(Type::ODS); - $reader->open('unsupported://foobar'); - } - - /** - * @expectedException \Box\Spout\Common\Exception\IOException - * - * @return void - */ - public function testReadWithSupportedCustomStreamWrapper() - { - /** @var \Box\Spout\Reader\ODS\Reader $reader */ - $reader = ReaderFactory::create(Type::ODS); - $reader->open('php://memory'); - } - - /** - * https://github.com/box/spout/issues/184 - * @return void - */ - public function testReadShouldInludeRowsWithZerosOnly() - { - $allRows = $this->getAllRowsForFile('sheet_with_zeros_in_row.ods'); - - $expectedRows = [ - ['A', 'B', 'C'], - ['1', '2', '3'], - ['0', '0', '0'] - ]; - $this->assertEquals($expectedRows, $allRows, 'There should be only 3 rows, because zeros (0) are valid values'); - } - - /** - * https://github.com/box/spout/issues/184 - * @return void - */ - public function testReadShouldCreateOutputEmptyCellPreserved() - { - $allRows = $this->getAllRowsForFile('sheet_with_empty_cells.ods'); - - $expectedRows = [ - ['A', 'B', 'C'], - ['0', '', ''], - ['1', '1', ''] - ]; - $this->assertEquals($expectedRows, $allRows, 'There should be 3 rows, with equal length'); - } - - /** - * https://github.com/box/spout/issues/195 - * @return void - */ - public function testReaderShouldNotTrimCellValues() - { - $allRows = $this->getAllRowsForFile('sheet_with_untrimmed_strings.ods'); - - $expectedRows = [ - ['A'], - [' A '], - ["\n\tA\n\t"], - ]; - - $this->assertEquals($expectedRows, $allRows, 'Cell values should not be trimmed'); - } - - /** - * https://github.com/box/spout/issues/218 - * @return void - */ - public function testReaderShouldReadTextInHyperlinks() - { - $allRows = $this->getAllRowsForFile('sheet_with_hyperlinks.ods'); - - $expectedRows = [ - ['email', 'text'], - ['1@example.com', 'text'], - ['2@example.com', 'text and https://github.com/box/spout/issues/218 and text'], - ]; - - $this->assertEquals($expectedRows, $allRows, 'Text in hyperlinks should be read'); - } - - /** - * @return void - */ - public function testReaderShouldReadInlineFontFormattingAsText() - { - $allRows = $this->getAllRowsForFile('sheet_with_inline_font_formatting.ods'); - - $expectedRows = [ - ['I am a yellow bird'] - ]; - - $this->assertEquals($expectedRows, $allRows, 'Text formatted inline should be read'); - } - - /** - * @param string $fileName - * @param bool|void $shouldFormatDates - * @param bool|void $shouldPreserveEmptyRows - * @return array All the read rows the given file - */ - private function getAllRowsForFile($fileName, $shouldFormatDates = false, $shouldPreserveEmptyRows = false) - { - $allRows = []; - $resourcePath = $this->getResourcePath($fileName); - - /** @var \Box\Spout\Reader\ODS\Reader $reader */ - $reader = ReaderFactory::create(Type::ODS); - $reader->setShouldFormatDates($shouldFormatDates); - $reader->setShouldPreserveEmptyRows($shouldPreserveEmptyRows); - $reader->open($resourcePath); - - foreach ($reader->getSheetIterator() as $sheetIndex => $sheet) { - foreach ($sheet->getRowIterator() as $rowIndex => $row) { - $allRows[] = $row; - } - } - - $reader->close(); - - return $allRows; - } -} diff --git a/tests/Spout/Reader/ODS/SheetTest.php b/tests/Spout/Reader/ODS/SheetTest.php deleted file mode 100644 index cc3bd03..0000000 --- a/tests/Spout/Reader/ODS/SheetTest.php +++ /dev/null @@ -1,66 +0,0 @@ -openFileAndReturnSheets('two_sheets_with_custom_names.ods'); - - $this->assertEquals('Sheet First', $sheets[0]->getName()); - $this->assertEquals(0, $sheets[0]->getIndex()); - $this->assertFalse($sheets[0]->isActive()); - - $this->assertEquals('Sheet Last', $sheets[1]->getName()); - $this->assertEquals(1, $sheets[1]->getIndex()); - $this->assertTrue($sheets[1]->isActive()); - } - - /** - * @return void - */ - public function testReaderShouldDefineFirstSheetAsActiveByDefault() - { - // NOTE: This spreadsheet has no information about the active sheet - $sheets = $this->openFileAndReturnSheets('two_sheets_with_no_settings_xml_file.ods'); - - $this->assertTrue($sheets[0]->isActive()); - $this->assertFalse($sheets[1]->isActive()); - } - - /** - * @param string $fileName - * @return Sheet[] - */ - private function openFileAndReturnSheets($fileName) - { - $resourcePath = $this->getResourcePath($fileName); - $reader = ReaderFactory::create(Type::ODS); - $reader->open($resourcePath); - - $sheets = []; - foreach ($reader->getSheetIterator() as $sheet) { - $sheets[] = $sheet; - } - - $reader->close(); - - return $sheets; - } -} diff --git a/tests/Spout/Reader/ReaderFactoryTest.php b/tests/Spout/Reader/ReaderFactoryTest.php deleted file mode 100644 index 57a0b55..0000000 --- a/tests/Spout/Reader/ReaderFactoryTest.php +++ /dev/null @@ -1,21 +0,0 @@ -getResourcePath('one_sheet_with_inline_strings.xlsx'); - - $xmlReader = new XMLReader(); - - // using "@" to prevent errors/warning to be displayed - $wasOpenSuccessful = @$xmlReader->openFileInZip($resourcePath, 'path/to/fake/file.xml'); - - $this->assertTrue($wasOpenSuccessful === false); - } - - /** - * Testing a HHVM bug: https://github.com/facebook/hhvm/issues/5779 - * The associated code in XMLReader::open() can be removed when the issue is fixed (and this test starts failing). - * @see XMLReader::open() - * - * @return void - */ - public function testHHVMStillDoesNotComplainWhenCallingOpenWithFileInsideZipNotExisting() - { - // Test should only be run on HHVM - if ($this->isRunningHHVM()) { - $resourcePath = $this->getResourcePath('one_sheet_with_inline_strings.xlsx'); - $nonExistingXMLFilePath = 'zip://' . $resourcePath . '#path/to/fake/file.xml'; - - libxml_clear_errors(); - $initialUseInternalErrorsSetting = libxml_use_internal_errors(true); - - // using the built-in XMLReader - $xmlReader = new \XMLReader(); - $this->assertTrue($xmlReader->open($nonExistingXMLFilePath) !== false); - $this->assertTrue(libxml_get_last_error() === false); - - libxml_use_internal_errors($initialUseInternalErrorsSetting); - } - } - - /** - * @return bool TRUE if running on HHVM, FALSE otherwise - */ - private function isRunningHHVM() - { - return defined('HHVM_VERSION'); - } - - /** - * @expectedException \Box\Spout\Reader\Exception\XMLProcessingException - * - * @return void - */ - public function testReadShouldThrowExceptionOnError() - { - $resourcePath = $this->getResourcePath('one_sheet_with_invalid_xml_characters.xlsx'); - - $xmlReader = new XMLReader(); - if ($xmlReader->openFileInZip($resourcePath, 'xl/worksheets/sheet1.xml') === false) { - $this->fail(); - } - - // using "@" to prevent errors/warning to be displayed - while (@$xmlReader->read()) { - // do nothing - } - } - - /** - * @expectedException \Box\Spout\Reader\Exception\XMLProcessingException - * - * @return void - */ - public function testNextShouldThrowExceptionOnError() - { - // The sharedStrings.xml file in "attack_billion_laughs.xlsx" contains - // a doctype element that causes read errors - $resourcePath = $this->getResourcePath('attack_billion_laughs.xlsx'); - - $xmlReader = new XMLReader(); - if ($xmlReader->openFileInZip($resourcePath, 'xl/sharedStrings.xml') !== false) { - @$xmlReader->next('sst'); - } - } - - /** - * @return array - */ - public function dataProviderForTestFileExistsWithinZip() - { - return [ - ['[Content_Types].xml', true], - ['xl/sharedStrings.xml', true], - ['xl/worksheets/sheet1.xml', true], - ['/invalid/file.xml', false], - ['another/invalid/file.xml', false], - ]; - } - - /** - * @dataProvider dataProviderForTestFileExistsWithinZip - * - * @param string $innerFilePath - * @param bool $expectedResult - * @return void - */ - public function testFileExistsWithinZip($innerFilePath, $expectedResult) - { - $resourcePath = $this->getResourcePath('one_sheet_with_inline_strings.xlsx'); - $zipStreamURI = 'zip://' . $resourcePath . '#' . $innerFilePath; - - $xmlReader = new XMLReader(); - $isZipStream = \ReflectionHelper::callMethodOnObject($xmlReader, 'fileExistsWithinZip', $zipStreamURI); - - $this->assertEquals($expectedResult, $isZipStream); - } - - /** - * @return array - */ - public function dataProviderForTestGetRealPathURIForFileInZip() - { - $tempFolder = realpath(sys_get_temp_dir()); - $tempFolderName = basename($tempFolder); - $expectedRealPathURI = 'zip://' . $tempFolder . '/test.xlsx#test.xml'; - - return [ - [$tempFolder, "$tempFolder/test.xlsx", 'test.xml', $expectedRealPathURI], - [$tempFolder, "$tempFolder/../$tempFolderName/test.xlsx", 'test.xml', $expectedRealPathURI], - ]; - } - - /** - * @dataProvider dataProviderForTestGetRealPathURIForFileInZip - * - * @param string $tempFolder - * @param string $zipFilePath - * @param string $fileInsideZipPath - * @param string $expectedRealPathURI - * @return void - */ - public function testGetRealPathURIForFileInZip($tempFolder, $zipFilePath, $fileInsideZipPath, $expectedRealPathURI) - { - touch($tempFolder . '/test.xlsx'); - - $xmlReader = new XMLReader(); - $realPathURI = \ReflectionHelper::callMethodOnObject($xmlReader, 'getRealPathURIForFileInZip', $zipFilePath, $fileInsideZipPath); - - // Normalizing path separators for Windows support - $normalizedRealPathURI = str_replace('\\', '/', $realPathURI); - $normalizedExpectedRealPathURI = str_replace('\\', '/', $expectedRealPathURI); - - $this->assertEquals($normalizedExpectedRealPathURI, $normalizedRealPathURI); - - unlink($tempFolder . '/test.xlsx'); - } - - /** - * @return array - */ - public function dataProviderForTestIsPositionedOnStartingAndEndingNode() - { - return [ - [''], // not prefixed - [''], // prefixed - ]; - } - - /** - * @dataProvider dataProviderForTestIsPositionedOnStartingAndEndingNode - * - * @param string $testXML - * @return void - */ - public function testIsPositionedOnStartingAndEndingNode($testXML) - { - $xmlReader = new XMLReader(); - $xmlReader->XML($testXML); - - // the first read moves the pointer to "" - $xmlReader->read(); - $this->assertTrue($xmlReader->isPositionedOnStartingNode('test')); - $this->assertFalse($xmlReader->isPositionedOnEndingNode('test')); - - // the seconds read moves the pointer to "" - $xmlReader->read(); - $this->assertFalse($xmlReader->isPositionedOnStartingNode('test')); - $this->assertTrue($xmlReader->isPositionedOnEndingNode('test')); - - $xmlReader->close(); - } -} diff --git a/tests/Spout/Reader/XLSX/Helper/CellHelperTest.php b/tests/Spout/Reader/XLSX/Helper/CellHelperTest.php deleted file mode 100644 index a410d9d..0000000 --- a/tests/Spout/Reader/XLSX/Helper/CellHelperTest.php +++ /dev/null @@ -1,71 +0,0 @@ - 1, 3 => 3], ['FILL', 1, 'FILL', 3] ] - ]; - } - - /** - * @dataProvider dataProviderForTestFillMissingArrayIndexes - * @param array $arrayToFill - * @param array $expectedFilledArray - */ - public function testFillMissingArrayIndexes($arrayToFill, array $expectedFilledArray) - { - $filledArray = CellHelper::fillMissingArrayIndexes($arrayToFill, 'FILL'); - $this->assertEquals($expectedFilledArray, $filledArray); - } - - /** - * @return array - */ - public function dataProviderForTestGetColumnIndexFromCellIndex() - { - return [ - ['A1', 0], - ['Z3', 25], - ['AA5', 26], - ['AB24', 27], - ['BC5', 54], - ['BCZ99', 1455], - ]; - } - - /** - * @dataProvider dataProviderForTestGetColumnIndexFromCellIndex - * - * @param string $cellIndex - * @param int $expectedColumnIndex - * @return void - */ - public function testGetColumnIndexFromCellIndex($cellIndex, $expectedColumnIndex) - { - $this->assertEquals($expectedColumnIndex, CellHelper::getColumnIndexFromCellIndex($cellIndex)); - } - - /** - * @expectedException \Box\Spout\Common\Exception\InvalidArgumentException - * - * @return void - */ - public function testGetColumnIndexFromCellIndexShouldThrowIfInvalidCellIndex() - { - CellHelper::getColumnIndexFromCellIndex('InvalidCellIndex'); - } -} diff --git a/tests/Spout/Reader/XLSX/Helper/CellValueFormatterTest.php b/tests/Spout/Reader/XLSX/Helper/CellValueFormatterTest.php deleted file mode 100644 index 96c71a9..0000000 --- a/tests/Spout/Reader/XLSX/Helper/CellValueFormatterTest.php +++ /dev/null @@ -1,175 +0,0 @@ -getMockBuilder('DOMNodeList')->disableOriginalConstructor()->getMock(); - - $nodeListMock - ->expects($this->atLeastOnce()) - ->method('item') - ->with(0) - ->will($this->returnValue((object)['nodeValue' => $nodeValue])); - - $nodeMock = $this->getMockBuilder('DOMElement')->disableOriginalConstructor()->getMock(); - - $nodeMock - ->expects($this->atLeastOnce()) - ->method('getAttribute') - ->will($this->returnValueMap([ - [CellValueFormatter::XML_ATTRIBUTE_TYPE, $cellType], - [CellValueFormatter::XML_ATTRIBUTE_STYLE_ID, 123], - ])); - - $nodeMock - ->expects($this->atLeastOnce()) - ->method('getElementsByTagName') - ->with(CellValueFormatter::XML_NODE_VALUE) - ->will($this->returnValue($nodeListMock)); - - $styleHelperMock = $this->getMockBuilder('Box\Spout\Reader\XLSX\Helper\StyleHelper')->disableOriginalConstructor()->getMock(); - - $styleHelperMock - ->expects($this->once()) - ->method('shouldFormatNumericValueAsDate') - ->with(123) - ->will($this->returnValue(true)); - - $formatter = new CellValueFormatter(null, $styleHelperMock, false); - $result = $formatter->extractAndFormatNodeValue($nodeMock); - - if ($expectedDateAsString === null) { - $this->assertNull($result); - } else { - $this->assertInstanceOf('DateTime', $result); - $this->assertSame($expectedDateAsString, $result->format('Y-m-d H:i:s')); - } - } - - /** - * @return array - */ - public function dataProviderForTestFormatNumericCellValueWithNumbers() - { - // Some test values exceed PHP_INT_MAX on 32-bit PHP. They are - // therefore converted to as doubles automatically by PHP. - $expectedBigNumberType = (PHP_INT_SIZE < 8 ? 'double' : 'integer'); - - return [ - [42, 42, 'integer'], - [42.5, 42.5, 'double'], - [-42, -42, 'integer'], - [-42.5, -42.5, 'double'], - ['42', 42, 'integer'], - ['42.5', 42.5, 'double'], - [865640023012945, 865640023012945, $expectedBigNumberType], - ['865640023012945', 865640023012945, $expectedBigNumberType], - [865640023012945.5, 865640023012945.5, 'double'], - ['865640023012945.5', 865640023012945.5, 'double'], - [PHP_INT_MAX, PHP_INT_MAX, 'integer'], - [~PHP_INT_MAX + 1, ~PHP_INT_MAX + 1, 'integer'], // ~PHP_INT_MAX === PHP_INT_MIN, PHP_INT_MIN being PHP7+ - [PHP_INT_MAX + 1, PHP_INT_MAX + 1, 'double'], - ]; - } - - /** - * @dataProvider dataProviderForTestFormatNumericCellValueWithNumbers - * - * @param int|float|string $value - * @param int|float $expectedFormattedValue - * @param string $expectedType - * @return void - */ - public function testFormatNumericCellValueWithNumbers($value, $expectedFormattedValue, $expectedType) - { - $styleHelperMock = $this->getMockBuilder('Box\Spout\Reader\XLSX\Helper\StyleHelper')->disableOriginalConstructor()->getMock(); - $styleHelperMock - ->expects($this->once()) - ->method('shouldFormatNumericValueAsDate') - ->will($this->returnValue(false)); - - $formatter = new CellValueFormatter(null, $styleHelperMock, false); - $formattedValue = \ReflectionHelper::callMethodOnObject($formatter, 'formatNumericCellValue', $value, 0); - - $this->assertEquals($expectedFormattedValue, $formattedValue); - $this->assertEquals($expectedType, gettype($formattedValue)); - } - - /** - * @return array - */ - public function dataProviderForTestFormatStringCellValue() - { - return [ - ['A', 'A'], - [' A ', ' A '], - ["\n\tA\n\t", "\n\tA\n\t"], - [' ', ' '], - ]; - } - - /** - * @dataProvider dataProviderForTestFormatStringCellValue - * - * @param string $value - * @param string $expectedFormattedValue - * @return void - */ - public function testFormatInlineStringCellValue($value, $expectedFormattedValue) - { - $nodeListMock = $this->getMockBuilder('DOMNodeList')->disableOriginalConstructor()->getMock(); - $nodeListMock - ->expects($this->atLeastOnce()) - ->method('item') - ->with(0) - ->will($this->returnValue((object)['nodeValue' => $value])); - - $nodeMock = $this->getMockBuilder('DOMElement')->disableOriginalConstructor()->getMock(); - $nodeMock - ->expects($this->atLeastOnce()) - ->method('getElementsByTagName') - ->with(CellValueFormatter::XML_NODE_INLINE_STRING_VALUE) - ->will($this->returnValue($nodeListMock)); - - $formatter = new CellValueFormatter(null, null, false); - $formattedValue = \ReflectionHelper::callMethodOnObject($formatter, 'formatInlineStringCellValue', $nodeMock); - - $this->assertEquals($expectedFormattedValue, $formattedValue); - } -} diff --git a/tests/Spout/Reader/XLSX/Helper/DateFormatHelperTest.php b/tests/Spout/Reader/XLSX/Helper/DateFormatHelperTest.php deleted file mode 100644 index cca02a7..0000000 --- a/tests/Spout/Reader/XLSX/Helper/DateFormatHelperTest.php +++ /dev/null @@ -1,49 +0,0 @@ -assertEquals($expectedPHPDateFormat, $phpDateFormat); - } -} diff --git a/tests/Spout/Reader/XLSX/Helper/SharedStringsCaching/CachingStrategyFactoryTest.php b/tests/Spout/Reader/XLSX/Helper/SharedStringsCaching/CachingStrategyFactoryTest.php deleted file mode 100644 index e61760a..0000000 --- a/tests/Spout/Reader/XLSX/Helper/SharedStringsCaching/CachingStrategyFactoryTest.php +++ /dev/null @@ -1,100 +0,0 @@ -getMockBuilder('\Box\Spout\Reader\XLSX\Helper\SharedStringsCaching\CachingStrategyFactory') - ->disableOriginalConstructor() - ->setMethods(['getMemoryLimitInKB']) - ->getMock(); - - $factoryStub->method('getMemoryLimitInKB')->willReturn($memoryLimitInKB); - - \ReflectionHelper::setStaticValue('\Box\Spout\Reader\XLSX\Helper\SharedStringsCaching\CachingStrategyFactory', 'instance', $factoryStub); - - $strategy = $factoryStub->getBestCachingStrategy($sharedStringsUniqueCount, null); - - $fullExpectedStrategyClassName = 'Box\Spout\Reader\XLSX\Helper\SharedStringsCaching\\' . $expectedStrategyClassName; - $this->assertEquals($fullExpectedStrategyClassName, get_class($strategy)); - - $strategy->clearCache(); - \ReflectionHelper::reset(); - } - - /** - * @return array - */ - public function dataProviderForTestGetMemoryLimitInKB() - { - return [ - ['-1', -1], - ['invalid', -1], - ['1024B', 1], - ['128K', 128], - ['256KB', 256], - ['512M', 512 * 1024], - ['2MB', 2 * 1024], - ['1G', 1 * 1024 * 1024], - ['10GB', 10 * 1024 * 1024], - ['2T', 2 * 1024 * 1024 * 1024], - ['5TB', 5 * 1024 * 1024 * 1024], - ]; - } - - /** - * @dataProvider dataProviderForTestGetMemoryLimitInKB - * - * @param string $memoryLimitFormatted - * @param float $expectedMemoryLimitInKB - * @return void - */ - public function testGetMemoryLimitInKB($memoryLimitFormatted, $expectedMemoryLimitInKB) - { - /** @var CachingStrategyFactory|\PHPUnit_Framework_MockObject_MockObject $factoryStub */ - $factoryStub = $this - ->getMockBuilder('\Box\Spout\Reader\XLSX\Helper\SharedStringsCaching\CachingStrategyFactory') - ->disableOriginalConstructor() - ->setMethods(['getMemoryLimitFromIni']) - ->getMock(); - - $factoryStub->method('getMemoryLimitFromIni')->willReturn($memoryLimitFormatted); - - $memoryLimitInKB = \ReflectionHelper::callMethodOnObject($factoryStub, 'getMemoryLimitInKB'); - - $this->assertEquals($expectedMemoryLimitInKB, $memoryLimitInKB); - } -} diff --git a/tests/Spout/Reader/XLSX/Helper/SharedStringsHelperTest.php b/tests/Spout/Reader/XLSX/Helper/SharedStringsHelperTest.php deleted file mode 100644 index 17d1b52..0000000 --- a/tests/Spout/Reader/XLSX/Helper/SharedStringsHelperTest.php +++ /dev/null @@ -1,144 +0,0 @@ -getResourcePath('one_sheet_with_shared_strings.xlsx'); - $this->sharedStringsHelper = new SharedStringsHelper($resourcePath); - } - - /** - * @return void - */ - public function tearDown() - { - $this->sharedStringsHelper->cleanup(); - } - - /** - * @expectedException \Box\Spout\Reader\Exception\SharedStringNotFoundException - * @return void - */ - public function testGetStringAtIndexShouldThrowExceptionIfStringNotFound() - { - $this->sharedStringsHelper->extractSharedStrings(); - $this->sharedStringsHelper->getStringAtIndex(PHP_INT_MAX); - } - - /** - * @return void - */ - public function testGetStringAtIndexShouldReturnTheCorrectStringIfFound() - { - $this->sharedStringsHelper->extractSharedStrings(); - - $sharedString = $this->sharedStringsHelper->getStringAtIndex(0); - $this->assertEquals('s1--A1', $sharedString); - - $sharedString = $this->sharedStringsHelper->getStringAtIndex(24); - $this->assertEquals('s1--E5', $sharedString); - - $usedCachingStrategy = \ReflectionHelper::getValueOnObject($this->sharedStringsHelper, 'cachingStrategy'); - $this->assertTrue($usedCachingStrategy instanceof InMemoryStrategy); - } - - /** - * @return void - */ - public function testGetStringAtIndexShouldWorkWithMultilineStrings() - { - $resourcePath = $this->getResourcePath('one_sheet_with_shared_multiline_strings.xlsx'); - $sharedStringsHelper = new SharedStringsHelper($resourcePath); - - $sharedStringsHelper->extractSharedStrings(); - - $sharedString = $sharedStringsHelper->getStringAtIndex(0); - $this->assertEquals("s1\nA1", $sharedString); - - $sharedString = $sharedStringsHelper->getStringAtIndex(24); - $this->assertEquals("s1\nE5", $sharedString); - - $sharedStringsHelper->cleanup(); - } - - /** - * @return void - */ - public function testGetStringAtIndexShouldWorkWithStringsContainingTextAndHyperlinkInSameCell() - { - $resourcePath = $this->getResourcePath('one_sheet_with_shared_strings_containing_text_and_hyperlink_in_same_cell.xlsx'); - $sharedStringsHelper = new SharedStringsHelper($resourcePath); - - $sharedStringsHelper->extractSharedStrings(); - - $sharedString = $sharedStringsHelper->getStringAtIndex(0); - $this->assertEquals('go to https://github.com please', $sharedString); - - $sharedStringsHelper->cleanup(); - } - - /** - * @return void - */ - public function testGetStringAtIndexShouldNotDoubleDecodeHTMLEntities() - { - $resourcePath = $this->getResourcePath('one_sheet_with_pre_encoded_html_entities.xlsx'); - $sharedStringsHelper = new SharedStringsHelper($resourcePath); - - $sharedStringsHelper->extractSharedStrings(); - - $sharedString = $sharedStringsHelper->getStringAtIndex(0); - $this->assertEquals('quote: " - ampersand: &', $sharedString); - - $sharedStringsHelper->cleanup(); - } - - /** - * @return void - */ - public function testGetStringAtIndexWithFileBasedStrategy() - { - // force the file-based strategy by setting no memory limit - $originalMemoryLimit = ini_get('memory_limit'); - ini_set('memory_limit', '-1'); - - $resourcePath = $this->getResourcePath('sheet_with_lots_of_shared_strings.xlsx'); - $sharedStringsHelper = new SharedStringsHelper($resourcePath); - - $sharedStringsHelper->extractSharedStrings(); - - $sharedString = $sharedStringsHelper->getStringAtIndex(0); - $this->assertEquals('str', $sharedString); - - $sharedString = $sharedStringsHelper->getStringAtIndex(CachingStrategyFactory::MAX_NUM_STRINGS_PER_TEMP_FILE + 1); - $this->assertEquals('str', $sharedString); - - $usedCachingStrategy = \ReflectionHelper::getValueOnObject($sharedStringsHelper, 'cachingStrategy'); - $this->assertTrue($usedCachingStrategy instanceof FileBasedStrategy); - - $sharedStringsHelper->cleanup(); - - ini_set('memory_limit', $originalMemoryLimit); - } -} diff --git a/tests/Spout/Reader/XLSX/Helper/StyleHelperTest.php b/tests/Spout/Reader/XLSX/Helper/StyleHelperTest.php deleted file mode 100644 index c06d94f..0000000 --- a/tests/Spout/Reader/XLSX/Helper/StyleHelperTest.php +++ /dev/null @@ -1,179 +0,0 @@ -getMockBuilder('\Box\Spout\Reader\XLSX\Helper\StyleHelper') - ->setConstructorArgs(['/path/to/file.xlsx']) - ->setMethods(['getCustomNumberFormats', 'getStylesAttributes']) - ->getMock(); - - $styleHelper->method('getStylesAttributes')->willReturn($styleAttributes); - $styleHelper->method('getCustomNumberFormats')->willReturn($customNumberFormats); - - return $styleHelper; - } - - /** - * @return void - */ - public function testShouldFormatNumericValueAsDateWithDefaultStyle() - { - $styleHelper = $this->getStyleHelperMock(); - $shouldFormatAsDate = $styleHelper->shouldFormatNumericValueAsDate(0); - $this->assertFalse($shouldFormatAsDate); - } - - /** - * @return void - */ - public function testShouldFormatNumericValueAsDateWhenShouldNotApplyNumberFormat() - { - $styleHelper = $this->getStyleHelperMock([[], ['applyNumberFormat' => false, 'numFmtId' => 14]]); - $shouldFormatAsDate = $styleHelper->shouldFormatNumericValueAsDate(1); - $this->assertFalse($shouldFormatAsDate); - } - - /** - * @return void - */ - public function testShouldFormatNumericValueAsDateWithGeneralFormat() - { - $styleHelper = $this->getStyleHelperMock([[], ['applyNumberFormat' => true, 'numFmtId' => 0]]); - $shouldFormatAsDate = $styleHelper->shouldFormatNumericValueAsDate(1); - $this->assertFalse($shouldFormatAsDate); - } - - /** - * @return void - */ - public function testShouldFormatNumericValueAsDateWithNonDateBuiltinFormat() - { - $styleHelper = $this->getStyleHelperMock([[], ['applyNumberFormat' => true, 'numFmtId' => 9]]); - $shouldFormatAsDate = $styleHelper->shouldFormatNumericValueAsDate(1); - $this->assertFalse($shouldFormatAsDate); - } - - /** - * @return void - */ - public function testShouldFormatNumericValueAsDateWithNoNumFmtId() - { - $styleHelper = $this->getStyleHelperMock([[], ['applyNumberFormat' => true, 'numFmtId' => null]]); - $shouldFormatAsDate = $styleHelper->shouldFormatNumericValueAsDate(1); - $this->assertFalse($shouldFormatAsDate); - } - - /** - * @return void - */ - public function testShouldFormatNumericValueAsDateWithBuiltinDateFormats() - { - $builtinNumFmtIdsForDate = [14, 15, 16, 17, 18, 19, 20, 21, 22, 45, 46, 47]; - - foreach ($builtinNumFmtIdsForDate as $builtinNumFmtIdForDate) { - $styleHelper = $this->getStyleHelperMock([[], ['applyNumberFormat' => true, 'numFmtId' => $builtinNumFmtIdForDate]]); - $shouldFormatAsDate = $styleHelper->shouldFormatNumericValueAsDate(1); - - $this->assertTrue($shouldFormatAsDate); - } - } - - /** - * @return void - */ - public function testShouldFormatNumericValueAsDateWhenApplyNumberFormatNotSetAndUsingBuiltinDateFormat() - { - $styleHelper = $this->getStyleHelperMock([[], ['applyNumberFormat' => null, 'numFmtId' => 14]]); - $shouldFormatAsDate = $styleHelper->shouldFormatNumericValueAsDate(1); - - $this->assertTrue($shouldFormatAsDate); - } - - /** - * @return void - */ - public function testShouldFormatNumericValueAsDateWhenApplyNumberFormatNotSetAndUsingBuiltinNonDateFormat() - { - $styleHelper = $this->getStyleHelperMock([[], ['applyNumberFormat' => null, 'numFmtId' => 9]]); - $shouldFormatAsDate = $styleHelper->shouldFormatNumericValueAsDate(1); - - $this->assertFalse($shouldFormatAsDate); - } - - /** - * @return void - */ - public function testShouldFormatNumericValueAsDateWhenCustomNumberFormatNotFound() - { - $styleHelper = $this->getStyleHelperMock([[], ['applyNumberFormat' => true, 'numFmtId' => 165]], [166 => []]); - $shouldFormatAsDate = $styleHelper->shouldFormatNumericValueAsDate(1); - - $this->assertFalse($shouldFormatAsDate); - } - - /** - * @return array - */ - public function dataProviderForCustomDateFormats() - { - return [ - // number format, expectedResult - ['[$-409]dddd\,\ mmmm\ d\,\ yy', true], - ['[$-409]d\-mmm\-yy;@', true], - ['[$-409]d\-mmm\-yyyy;@', true], - ['mm/dd/yy;@', true], - ['MM/DD/YY;@', true], - ['[$-F800]dddd\,\ mmmm\ dd\,\ yyyy', true], - ['m/d;@', true], - ['m/d/yy;@', true], - ['[$-409]d\-mmm;@', true], - ['[$-409]dd\-mmm\-yy;@', true], - ['[$-409]mmm\-yy;@', true], - ['[$-409]mmmm\-yy;@', true], - ['[$-409]mmmm\ d\,\ yyyy;@', true], - ['[$-409]m/d/yy\ h:mm\ AM/PM;@', true], - ['m/d/yy\ h:mm;@', true], - ['[$-409]mmmmm;@', true], - ['[$-409]MMmmM;@', true], - ['[$-409]mmmmm\-yy;@', true], - ['m/d/yyyy;@', true], - ['[$-409]m/d/yy\--h:mm;@', true], - ['General', false], - ['GENERAL', false], - ['\ma\yb\e', false], - ['[Red]foo;', false], - ]; - } - - /** - * @dataProvider dataProviderForCustomDateFormats - * - * @param string $numberFormat - * @param bool $expectedResult - * @return void - */ - public function testShouldFormatNumericValueAsDateWithCustomDateFormats($numberFormat, $expectedResult) - { - $numFmtId = 165; - $styleHelper = $this->getStyleHelperMock([[], ['applyNumberFormat' => true, 'numFmtId' => $numFmtId]], [$numFmtId => $numberFormat]); - $shouldFormatAsDate = $styleHelper->shouldFormatNumericValueAsDate(1); - - $this->assertEquals($expectedResult, $shouldFormatAsDate); - } -} diff --git a/tests/Spout/Reader/XLSX/ReaderTest.php b/tests/Spout/Reader/XLSX/ReaderTest.php deleted file mode 100644 index b9e032e..0000000 --- a/tests/Spout/Reader/XLSX/ReaderTest.php +++ /dev/null @@ -1,649 +0,0 @@ -getAllRowsForFile($filePath); - } - - /** - * @return array - */ - public function dataProviderForTestReadForAllWorksheets() - { - return [ - ['one_sheet_with_shared_strings.xlsx', 5, 5], - ['one_sheet_with_inline_strings.xlsx', 5, 5], - ['two_sheets_with_shared_strings.xlsx', 10, 5], - ['two_sheets_with_inline_strings.xlsx', 10, 5] - ]; - } - - /** - * @dataProvider dataProviderForTestReadForAllWorksheets - * - * @param string $resourceName - * @param int $expectedNumOfRows - * @param int $expectedNumOfCellsPerRow - * @return void - */ - public function testReadForAllWorksheets($resourceName, $expectedNumOfRows, $expectedNumOfCellsPerRow) - { - $allRows = $this->getAllRowsForFile($resourceName); - - $this->assertEquals($expectedNumOfRows, count($allRows), "There should be $expectedNumOfRows rows"); - foreach ($allRows as $row) { - $this->assertEquals($expectedNumOfCellsPerRow, count($row), "There should be $expectedNumOfCellsPerRow cells for every row"); - } - } - - /** - * @return void - */ - public function testReadShouldSupportSheetsDefinitionInRandomOrder() - { - $allRows = $this->getAllRowsForFile('two_sheets_with_sheets_definition_in_reverse_order.xlsx'); - - $expectedRows = [ - ['s1 - A1', 's1 - B1', 's1 - C1', 's1 - D1', 's1 - E1'], - ['s1 - A2', 's1 - B2', 's1 - C2', 's1 - D2', 's1 - E2'], - ['s1 - A3', 's1 - B3', 's1 - C3', 's1 - D3', 's1 - E3'], - ['s1 - A4', 's1 - B4', 's1 - C4', 's1 - D4', 's1 - E4'], - ['s1 - A5', 's1 - B5', 's1 - C5', 's1 - D5', 's1 - E5'], - ['s2 - A1', 's2 - B1', 's2 - C1', 's2 - D1', 's2 - E1'], - ['s2 - A2', 's2 - B2', 's2 - C2', 's2 - D2', 's2 - E2'], - ['s2 - A3', 's2 - B3', 's2 - C3', 's2 - D3', 's2 - E3'], - ['s2 - A4', 's2 - B4', 's2 - C4', 's2 - D4', 's2 - E4'], - ['s2 - A5', 's2 - B5', 's2 - C5', 's2 - D5', 's2 - E5'], - ]; - $this->assertEquals($expectedRows, $allRows); - } - - /** - * @return void - */ - public function testReadShouldSupportPrefixedXMLFiles() - { - // The XML files of this spreadsheet are prefixed. - // For instance, they use "" instead of "", etc. - $allRows = $this->getAllRowsForFile('sheet_with_prefixed_xml_files.xlsx'); - - $expectedRows = [ - ['s1 - A1', 's1 - B1', 's1 - C1'], - ['s1 - A2', 's1 - B2', 's1 - C2'], - ['s1 - A3', 's1 - B3', 's1 - C3'], - ]; - $this->assertEquals($expectedRows, $allRows); - } - - /** - * @return void - */ - public function testReadShouldSupportSheetWithSharedStringsMissingUniqueCountAttribute() - { - $allRows = $this->getAllRowsForFile('one_sheet_with_shared_strings_missing_unique_count.xlsx'); - - $expectedRows = [ - ['s1--A1', 's1--B1'], - ['s1--A2', 's1--B2'], - ]; - $this->assertEquals($expectedRows, $allRows); - } - - /** - * @return void - */ - public function testReadShouldSupportSheetWithSharedStringsMissingUniqueCountAndCountAttributes() - { - $allRows = $this->getAllRowsForFile('one_sheet_with_shared_strings_missing_unique_count_and_count.xlsx'); - - $expectedRows = [ - ['s1--A1', 's1--B1'], - ['s1--A2', 's1--B2'], - ]; - $this->assertEquals($expectedRows, $allRows); - } - - /** - * @return void - */ - public function testReadShouldSupportFilesWithoutSharedStringsFile() - { - $allRows = $this->getAllRowsForFile('sheet_with_no_shared_strings_file.xlsx'); - - $expectedRows = [ - [10, 11], - [20, 21], - ]; - $this->assertEquals($expectedRows, $allRows); - } - - /** - * @return void - */ - public function testReadShouldSupportFilesWithoutCellReference() - { - // file where the cell definition does not have a "r" attribute - // as in ... - $allRows = $this->getAllRowsForFile('sheet_with_missing_cell_reference.xlsx'); - - $expectedRows = [ - ['s1--A1'], - ]; - $this->assertEquals($expectedRows, $allRows); - } - - /** - * @return void - */ - public function testReadShouldSupportAllCellTypes() - { - // make sure dates are always created with the same timezone - date_default_timezone_set('UTC'); - - $allRows = $this->getAllRowsForFile('sheet_with_all_cell_types.xlsx'); - - $expectedRows = [ - [ - 's1--A1', 's1--A2', - false, true, - \DateTime::createFromFormat('Y-m-d H:i:s', '2015-06-03 13:21:58'), - \DateTime::createFromFormat('Y-m-d H:i:s', '2015-06-01 00:00:00'), - 10, 10.43, - null, - 'weird string', // valid 'str' string - null, // invalid date - ], - ['', '', '', '', '', '', '', '', ''], - ]; - $this->assertEquals($expectedRows, $allRows); - } - - /** - * @return void - */ - public function testReadShouldSupportNumericTimestampFormattedDifferentlyAsDate() - { - // make sure dates are always created with the same timezone - date_default_timezone_set('UTC'); - - $allRows = $this->getAllRowsForFile('sheet_with_same_numeric_value_date_formatted_differently.xlsx'); - - $expectedDate = \DateTime::createFromFormat('Y-m-d H:i:s', '2015-01-01 00:00:00'); - $expectedRows = [ - array_fill(0, 10, $expectedDate), - array_fill(0, 10, $expectedDate), - array_fill(0, 10, $expectedDate), - array_merge(array_fill(0, 7, $expectedDate), ['', '', '']), - ]; - - $this->assertEquals($expectedRows, $allRows); - } - - /** - * @return void - */ - public function testReadShouldSupportDifferentDatesAsNumericTimestamp() - { - // make sure dates are always created with the same timezone - date_default_timezone_set('UTC'); - - $allRows = $this->getAllRowsForFile('sheet_with_different_numeric_value_dates.xlsx'); - - $expectedRows = [ - [ - \DateTime::createFromFormat('Y-m-d H:i:s', '2015-09-01 00:00:00'), - \DateTime::createFromFormat('Y-m-d H:i:s', '2015-09-02 00:00:00'), - \DateTime::createFromFormat('Y-m-d H:i:s', '2015-09-01 22:23:00'), - ], - [ - \DateTime::createFromFormat('Y-m-d H:i:s', '1900-02-28 23:59:59'), - \DateTime::createFromFormat('Y-m-d H:i:s', '1900-03-01 00:00:00'), - \DateTime::createFromFormat('Y-m-d H:i:s', '1900-02-28 11:00:00'), // 1900-02-29 should be converted to 1900-02-28 - ] - ]; - $this->assertEquals($expectedRows, $allRows); - } - - /** - * @return void - */ - public function testReadShouldSupportDifferentTimesAsNumericTimestamp() - { - // make sure dates are always created with the same timezone - date_default_timezone_set('UTC'); - - $allRows = $this->getAllRowsForFile('sheet_with_different_numeric_value_times.xlsx'); - - $expectedRows = [ - [ - \DateTime::createFromFormat('Y-m-d H:i:s', '1900-01-01 00:00:00'), - \DateTime::createFromFormat('Y-m-d H:i:s', '1900-01-01 11:29:00'), - \DateTime::createFromFormat('Y-m-d H:i:s', '1900-01-01 23:29:00'), - \DateTime::createFromFormat('Y-m-d H:i:s', '1900-01-01 01:42:25'), - \DateTime::createFromFormat('Y-m-d H:i:s', '1900-01-01 13:42:25'), - ] - ]; - $this->assertEquals($expectedRows, $allRows); - } - - /** - * @return void - */ - public function testReadShouldSupportFormatDatesAndTimesIfSpecified() - { - $shouldFormatDates = true; - $allRows = $this->getAllRowsForFile('sheet_with_dates_and_times.xlsx', $shouldFormatDates); - - $expectedRows = [ - ['1/13/2016', '01/13/2016', '13-Jan-16', 'Wednesday January 13, 16', 'Today is 1/13/2016'], - ['4:43:25', '04:43', '4:43', '4:43:25 AM', '4:43:25 PM'], - ['1976-11-22T08:30:00.000', '1976-11-22T08:30', '1582-10-15', '08:30:00', '08:30'], - ]; - $this->assertEquals($expectedRows, $allRows); - } - - /** - * @return void - */ - public function testReadShouldApplyCustomDateFormatNumberEvenIfApplyNumberFormatNotSpecified() - { - $shouldFormatDates = true; - $allRows = $this->getAllRowsForFile('sheet_with_custom_date_formats_and_no_apply_number_format.xlsx', $shouldFormatDates); - - $expectedRows = [ - // "General", "GENERAL", "MM/DD/YYYY", "MM/dd/YYYY", "H:MM:SS" - ['42382', '42382', '01/13/2016', '01/13/2016', '4:43:25'], - ]; - $this->assertEquals($expectedRows, $allRows); - } - - /** - * @return void - */ - public function testReadShouldKeepEmptyCellsAtTheEndIfDimensionsSpecified() - { - $allRows = $this->getAllRowsForFile('sheet_without_dimensions_but_spans_and_empty_cells.xlsx'); - - $this->assertEquals(2, count($allRows), 'There should be 2 rows'); - foreach ($allRows as $row) { - $this->assertEquals(5, count($row), 'There should be 5 cells for every row, because empty rows should be preserved'); - } - - $expectedRows = [ - ['s1--A1', 's1--B1', 's1--C1', 's1--D1', 's1--E1'], - ['s1--A2', 's1--B2', 's1--C2', '', ''], - ]; - $this->assertEquals($expectedRows, $allRows); - } - - /** - * @return void - */ - public function testReadShouldKeepEmptyCellsAtTheEndIfNoDimensionsButSpansSpecified() - { - $allRows = $this->getAllRowsForFile('sheet_without_dimensions_and_empty_cells.xlsx'); - - $this->assertEquals(2, count($allRows), 'There should be 2 rows'); - $this->assertEquals(5, count($allRows[0]), 'There should be 5 cells in the first row'); - $this->assertEquals(3, count($allRows[1]), 'There should be only 3 cells in the second row, because empty rows at the end should be skip'); - - $expectedRows = [ - ['s1--A1', 's1--B1', 's1--C1', 's1--D1', 's1--E1'], - ['s1--A2', 's1--B2', 's1--C2'], - ]; - $this->assertEquals($expectedRows, $allRows); - } - - /** - * @return void - */ - public function testReadShouldSkipEmptyCellsAtTheEndIfDimensionsNotSpecified() - { - $allRows = $this->getAllRowsForFile('sheet_without_dimensions_and_empty_cells.xlsx'); - - $this->assertEquals(2, count($allRows), 'There should be 2 rows'); - $this->assertEquals(5, count($allRows[0]), 'There should be 5 cells in the first row'); - $this->assertEquals(3, count($allRows[1]), 'There should be only 3 cells in the second row, because empty rows at the end should be skip'); - - $expectedRows = [ - ['s1--A1', 's1--B1', 's1--C1', 's1--D1', 's1--E1'], - ['s1--A2', 's1--B2', 's1--C2'], - ]; - $this->assertEquals($expectedRows, $allRows); - } - - /** - * @return void - */ - public function testReadShouldSkipEmptyRowsIfShouldPreserveEmptyRowsNotSet() - { - $allRows = $this->getAllRowsForFile('sheet_with_empty_rows_and_missing_row_index.xlsx'); - - $this->assertEquals(3, count($allRows), 'There should be only 3 rows, because the empty rows are skipped'); - - $expectedRows = [ - // skipped row here - ['s1--A2', 's1--B2', 's1--C2'], - // skipped row here - // skipped row here - ['s1--A5', 's1--B5', 's1--C5'], - ['s1--A6', 's1--B6', 's1--C6'], - ]; - $this->assertEquals($expectedRows, $allRows); - } - - /** - * @return void - */ - public function testReadShouldReturnEmptyLinesIfShouldPreserveEmptyRowsSet() - { - $allRows = $this->getAllRowsForFile('sheet_with_empty_rows_and_missing_row_index.xlsx', false, true); - - $this->assertEquals(6, count($allRows), 'There should be 6 rows'); - - $expectedRows = [ - [''], - ['s1--A2', 's1--B2', 's1--C2'], - [''], - [''], - ['s1--A5', 's1--B5', 's1--C5'], - ['s1--A6', 's1--B6', 's1--C6'], - ]; - $this->assertEquals($expectedRows, $allRows); - } - - /** - * @return void - */ - public function testReadShouldSupportEmptySharedString() - { - $allRows = $this->getAllRowsForFile('sheet_with_empty_shared_string.xlsx'); - - $expectedRows = [ - ['s1--A1', '', 's1--C1'], - ]; - $this->assertEquals($expectedRows, $allRows); - } - - /** - * @return void - */ - public function testReadShouldPreserveSpaceIfSpecified() - { - $allRows = $this->getAllRowsForFile('sheet_with_preserve_space_shared_strings.xlsx'); - - $expectedRows = [ - [' s1--A1', 's1--B1 ', ' s1--C1 '], - ]; - $this->assertEquals($expectedRows, $allRows); - } - - /** - * @return void - */ - public function testReadShouldSkipPronunciationData() - { - $allRows = $this->getAllRowsForFile('sheet_with_pronunciation.xlsx'); - - $expectedRow = ['名前', '一二三四']; - $this->assertEquals($expectedRow, $allRows[0], 'Pronunciation data should be removed.'); - } - - /** - * @NOTE: The LIBXML_NOENT is used to ACTUALLY substitute entities (and should therefore not be used) - * - * @return void - */ - public function testReadShouldBeProtectedAgainstBillionLaughsAttack() - { - $startTime = microtime(true); - - try { - // using @ to prevent warnings/errors from being displayed - @$this->getAllRowsForFile('attack_billion_laughs.xlsx'); - $this->fail('An exception should have been thrown'); - } catch (IOException $exception) { - $duration = microtime(true) - $startTime; - $this->assertLessThan(10, $duration, 'Entities should not be expanded and therefore take more than 10 seconds to be parsed.'); - - $expectedMaxMemoryUsage = 40 * 1024 * 1024; // 40MB - $this->assertLessThan($expectedMaxMemoryUsage, memory_get_peak_usage(true), 'Entities should not be expanded and therefore consume all the memory.'); - } - } - - /** - * @NOTE: The LIBXML_NOENT is used to ACTUALLY substitute entities (and should therefore not be used) - * - * @return void - */ - public function testReadShouldBeProtectedAgainstQuadraticBlowupAttack() - { - $startTime = microtime(true); - - $this->getAllRowsForFile('attack_quadratic_blowup.xlsx'); - - $duration = microtime(true) - $startTime; - $this->assertLessThan(10, $duration, 'Entities should not be expanded and therefore take more than 10 seconds to be parsed.'); - - $expectedMaxMemoryUsage = 40 * 1024 * 1024; // 40MB - $this->assertLessThan($expectedMaxMemoryUsage, memory_get_peak_usage(true), 'Entities should not be expanded and therefore consume all the memory.'); - } - - /** - * @return void - */ - public function testReadShouldBeAbleToProcessEmptySheets() - { - $allRows = $this->getAllRowsForFile('sheet_with_no_cells.xlsx'); - $this->assertEquals([], $allRows, 'Sheet with no cells should be correctly processed.'); - } - - /** - * @return void - */ - public function testReadShouldSkipFormulas() - { - $allRows = $this->getAllRowsForFile('sheet_with_formulas.xlsx'); - - $expectedRows = [ - ['val1', 'val2', 'total1', 'total2'], - [10, 20, 30, 21], - [11, 21, 32, 41], - ]; - $this->assertEquals($expectedRows, $allRows); - } - - /** - * @return void - */ - public function testReadMultipleTimesShouldRewindReader() - { - $allRows = []; - $resourcePath = $this->getResourcePath('two_sheets_with_inline_strings.xlsx'); - - $reader = ReaderFactory::create(Type::XLSX); - $reader->open($resourcePath); - - foreach ($reader->getSheetIterator() as $sheet) { - // do nothing - } - - foreach ($reader->getSheetIterator() as $sheet) { - // this loop should only add the first row of the first sheet - foreach ($sheet->getRowIterator() as $row) { - $allRows[] = $row; - break; - } - - // this loop should rewind the iterator and restart reading from the 1st row again - // therefore, it should only add the first row of the first sheet - foreach ($sheet->getRowIterator() as $row) { - $allRows[] = $row; - break; - } - - // not reading any more sheets - break; - } - - foreach ($reader->getSheetIterator() as $sheet) { - // this loop should only add the first row of the current sheet - foreach ($sheet->getRowIterator() as $row) { - $allRows[] = $row; - break; - } - - // not breaking, so we keep reading the next sheets - } - - $reader->close(); - - $expectedRows = [ - ['s1 - A1', 's1 - B1', 's1 - C1', 's1 - D1', 's1 - E1'], - ['s1 - A1', 's1 - B1', 's1 - C1', 's1 - D1', 's1 - E1'], - ['s1 - A1', 's1 - B1', 's1 - C1', 's1 - D1', 's1 - E1'], - ['s2 - A1', 's2 - B1', 's2 - C1', 's2 - D1', 's2 - E1'], - ]; - $this->assertEquals($expectedRows, $allRows); - } - - /** - * @expectedException \Box\Spout\Common\Exception\IOException - * - * @return void - */ - public function testReadWithUnsupportedCustomStreamWrapper() - { - /** @var \Box\Spout\Reader\XLSX\Reader $reader */ - $reader = ReaderFactory::create(Type::XLSX); - $reader->open('unsupported://foobar'); - } - - /** - * @expectedException \Box\Spout\Common\Exception\IOException - * - * @return void - */ - public function testReadWithSupportedCustomStreamWrapper() - { - /** @var \Box\Spout\Reader\XLSX\Reader $reader */ - $reader = ReaderFactory::create(Type::XLSX); - $reader->open('php://memory'); - } - - /** - * https://github.com/box/spout/issues/184 - * @return void - */ - public function testReadShouldInludeRowsWithZerosOnly() - { - $allRows = $this->getAllRowsForFile('sheet_with_zeros_in_row.xlsx'); - - $expectedRows = [ - ['A', 'B', 'C'], - ['1', '2', '3'], - ['0', '0', '0'] - ]; - $this->assertEquals($expectedRows, $allRows, 'There should be only 3 rows, because zeros (0) are valid values'); - } - - /** - * https://github.com/box/spout/issues/184 - * @return void - */ - public function testReadShouldCreateOutputEmptyCellPreserved() - { - $allRows = $this->getAllRowsForFile('sheet_with_empty_cells.xlsx'); - - $expectedRows = [ - ['A', 'B', 'C'], - ['0', '', ''], - ['1', '1', ''] - ]; - $this->assertEquals($expectedRows, $allRows, 'There should be 3 rows, with equal length'); - } - - - /** - * https://github.com/box/spout/issues/195 - * @return void - */ - public function testReaderShouldNotTrimCellValues() - { - $allRows = $this->getAllRowsForFile('sheet_with_untrimmed_inline_strings.xlsx'); - - $expectedRows = [ - ['A'], - [' A '], - ["\n\tA\n\t"], - ]; - - $this->assertEquals($expectedRows, $allRows, 'Cell values should not be trimmed'); - } - - - /** - * @param string $fileName - * @param bool|void $shouldFormatDates - * @param bool|void $shouldPreserveEmptyRows - * @return array All the read rows the given file - */ - private function getAllRowsForFile($fileName, $shouldFormatDates = false, $shouldPreserveEmptyRows = false) - { - $allRows = []; - $resourcePath = $this->getResourcePath($fileName); - - /** @var \Box\Spout\Reader\XLSX\Reader $reader */ - $reader = ReaderFactory::create(Type::XLSX); - $reader->setShouldFormatDates($shouldFormatDates); - $reader->setShouldPreserveEmptyRows($shouldPreserveEmptyRows); - $reader->open($resourcePath); - - foreach ($reader->getSheetIterator() as $sheetIndex => $sheet) { - foreach ($sheet->getRowIterator() as $rowIndex => $row) { - $allRows[] = $row; - } - } - - $reader->close(); - - return $allRows; - } -} diff --git a/tests/Spout/Reader/XLSX/SheetTest.php b/tests/Spout/Reader/XLSX/SheetTest.php deleted file mode 100644 index b8c332b..0000000 --- a/tests/Spout/Reader/XLSX/SheetTest.php +++ /dev/null @@ -1,54 +0,0 @@ -openFileAndReturnSheets('two_sheets_with_custom_names_and_custom_active_tab.xlsx'); - - $this->assertEquals('CustomName1', $sheets[0]->getName()); - $this->assertEquals(0, $sheets[0]->getIndex()); - $this->assertFalse($sheets[0]->isActive()); - - $this->assertEquals('CustomName2', $sheets[1]->getName()); - $this->assertEquals(1, $sheets[1]->getIndex()); - $this->assertTrue($sheets[1]->isActive()); - } - - /** - * @param string $fileName - * @return Sheet[] - */ - private function openFileAndReturnSheets($fileName) - { - $resourcePath = $this->getResourcePath($fileName); - $reader = ReaderFactory::create(Type::XLSX); - $reader->open($resourcePath); - - $sheets = []; - foreach ($reader->getSheetIterator() as $sheet) { - $sheets[] = $sheet; - } - - $reader->close(); - - return $sheets; - } -} diff --git a/tests/Spout/ReflectionHelper.php b/tests/Spout/ReflectionHelper.php deleted file mode 100644 index df02de8..0000000 --- a/tests/Spout/ReflectionHelper.php +++ /dev/null @@ -1,115 +0,0 @@ - $valueNames) { - foreach ($valueNames as $valueName => $originalValue) { - self::setStaticValue($class, $valueName, $originalValue, $saveOriginalValue = false); - } - } - self::$privateVarsToReset = array(); - } - - /** - * Get the value of a static private or public class property. - * Used to test internals of class without having to make the property public - * - * @param string $class - * @param string $valueName - * @return mixed|null - */ - public static function getStaticValue($class, $valueName) - { - $reflectionClass = new ReflectionClass($class); - $reflectionProperty = $reflectionClass->getProperty($valueName); - $reflectionProperty->setAccessible(true); - $value = $reflectionProperty->getValue(); - - // clean up - $reflectionProperty->setAccessible(false); - - return $value; - } - - /** - * Set the value of a static private or public class property. - * Used to test internals of class without having to make the property public - * - * @param string $class - * @param string $valueName - * @param mixed|null $value - * @param bool|void $saveOriginalValue - * @return void - */ - public static function setStaticValue($class, $valueName, $value, $saveOriginalValue = true) - { - $reflectionClass = new ReflectionClass($class); - $reflectionProperty = $reflectionClass->getProperty($valueName); - $reflectionProperty->setAccessible(true); - - // to prevent side-effects in later tests, we need to remember the original value and reset it on tear down - // @NOTE: we need to check isset in case the original value was null or array() - if ($saveOriginalValue && (!isset(self::$privateVarsToReset[$class]) || !isset(self::$privateVarsToReset[$class][$name]))) { - self::$privateVarsToReset[$class][$valueName] = $reflectionProperty->getValue(); - } - $reflectionProperty->setValue($value); - - // clean up - $reflectionProperty->setAccessible(false); - } - - /** - * @param object $object - * @param string $valueName - * - * @return mixed|null - */ - public static function getValueOnObject($object, $valueName) - { - $reflectionObject = new ReflectionObject($object); - $reflectionProperty = $reflectionObject->getProperty($valueName); - $reflectionProperty->setAccessible(true); - $value = $reflectionProperty->getValue($object); - - // clean up - $reflectionProperty->setAccessible(false); - - return $value; - } - - /** - * Invoke a the given public or protected method on the given object. - * - * @param object $object - * @param string $methodName - * @param *mixed|null $params - * - * @return mixed|null - */ - public static function callMethodOnObject($object, $methodName) - { - $params = func_get_args(); - array_shift($params); // object - array_shift($params); // methodName - - $className = get_class($object); - $class = new ReflectionClass($className); - $method = $class->getMethod($methodName); - $method->setAccessible(true); - - return $method->invokeArgs($object, $params); - } -} diff --git a/tests/Spout/TestUsingResource.php b/tests/Spout/TestUsingResource.php deleted file mode 100644 index 2b200d9..0000000 --- a/tests/Spout/TestUsingResource.php +++ /dev/null @@ -1,141 +0,0 @@ -resourcesPath) . '/' . strtolower($resourceType) . '/' . $resourceName; - - return (file_exists($resourcePath) ? $resourcePath : null); - } - - /** - * @param string $resourceName - * @return string Path of the generated resource for the given name - */ - protected function getGeneratedResourcePath($resourceName) - { - $resourceType = pathinfo($resourceName, PATHINFO_EXTENSION); - $generatedResourcePath = realpath($this->generatedResourcesPath) . '/' . strtolower($resourceType) . '/' . $resourceName; - - return $generatedResourcePath; - } - - /** - * @param string $resourceName - * @return void - */ - protected function createGeneratedFolderIfNeeded($resourceName) - { - $resourceType = pathinfo($resourceName, PATHINFO_EXTENSION); - $generatedResourcePathForType = $this->generatedResourcesPath . '/' . strtolower($resourceType); - - if (!file_exists($generatedResourcePathForType)) { - mkdir($generatedResourcePathForType, 0777, true); - } - } - - /** - * @param string $resourceName - * @return string Path of the generated unwritable (because parent folder is read only) resource for the given name - */ - protected function getGeneratedUnwritableResourcePath($resourceName) - { - return realpath($this->generatedUnwritableResourcesPath) . '/' . $resourceName; - } - - /** - * @return void - */ - protected function createUnwritableFolderIfNeeded() - { - // On Windows, chmod() or the mkdir's mode is ignored - if ($this->isWindows()) { - $this->markTestSkipped('Skipping because Windows cannot create read-only folders through PHP'); - } - - if (!file_exists($this->generatedUnwritableResourcesPath)) { - // Make sure generated folder exists first - if (!file_exists($this->generatedResourcesPath)) { - mkdir($this->generatedResourcesPath, 0777, true); - } - - // 0444 = read only - mkdir($this->generatedUnwritableResourcesPath, 0444, true); - } - } - - /** - * @return string Path of the temp folder - */ - protected function getTempFolderPath() - { - return realpath($this->tempFolderPath); - } - - /** - * @return void - */ - protected function recreateTempFolder() - { - if (file_exists($this->tempFolderPath)) { - $this->deleteFolderRecursively($this->tempFolderPath); - } - - mkdir($this->tempFolderPath, 0777, true); - } - - /** - * @param string $folderPath - * @return void - */ - private function deleteFolderRecursively($folderPath) - { - $itemIterator = new \RecursiveIteratorIterator( - new \RecursiveDirectoryIterator($folderPath, \RecursiveDirectoryIterator::SKIP_DOTS), - \RecursiveIteratorIterator::CHILD_FIRST - ); - - foreach ($itemIterator as $item) { - if ($item->isDir()) { - rmdir($item->getPathname()); - } else { - unlink($item->getPathname()); - } - } - - rmdir($folderPath); - } - - /** - * @return bool Whether the OS on which PHP is installed is Windows - */ - protected function isWindows() - { - return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN'; - } -} diff --git a/tests/Spout/Writer/CSV/WriterTest.php b/tests/Spout/Writer/CSV/WriterTest.php deleted file mode 100644 index 9558129..0000000 --- a/tests/Spout/Writer/CSV/WriterTest.php +++ /dev/null @@ -1,214 +0,0 @@ -createUnwritableFolderIfNeeded(); - $filePath = $this->getGeneratedUnwritableResourcePath($fileName); - - $writer = WriterFactory::create(Type::CSV); - @$writer->openToFile($filePath); - $writer->addRow(['csv--11', 'csv--12']); - $writer->close(); - } - - /** - * @expectedException \Box\Spout\Writer\Exception\WriterNotOpenedException - */ - public function testWriteShouldThrowExceptionIfCallAddRowBeforeOpeningWriter() - { - $writer = WriterFactory::create(Type::CSV); - $writer->addRow(['csv--11', 'csv--12']); - $writer->close(); - } - - /** - * @expectedException \Box\Spout\Writer\Exception\WriterNotOpenedException - */ - public function testWriteShouldThrowExceptionIfCallAddRowsBeforeOpeningWriter() - { - $writer = WriterFactory::create(Type::CSV); - $writer->addRows([['csv--11', 'csv--12']]); - $writer->close(); - } - - /** - * @expectedException \Box\Spout\Common\Exception\InvalidArgumentException - */ - public function testAddRowsShouldThrowExceptionIfRowsAreNotArrayOfArrays() - { - $writer = WriterFactory::create(Type::CSV); - $writer->addRows(['csv--11', 'csv--12']); - $writer->close(); - } - - /** - * @return void - */ - public function testCloseShouldNoopWhenWriterIsNotOpened() - { - $fileName = 'test_double_close_calls.csv'; - $this->createGeneratedFolderIfNeeded($fileName); - $resourcePath = $this->getGeneratedResourcePath($fileName); - - $writer = WriterFactory::create(Type::CSV); - $writer->close(); // This call should not cause any error - - $writer->openToFile($resourcePath); - $writer->close(); - $writer->close(); // This call should not cause any error - } - - /** - * @return void - */ - public function testWriteShouldAddUtf8Bom() - { - $allRows = [ - ['csv--11', 'csv--12'], - ]; - $writtenContent = $this->writeToCsvFileAndReturnWrittenContent($allRows, 'csv_with_utf8_bom.csv'); - - $this->assertContains(EncodingHelper::BOM_UTF8, $writtenContent, 'The CSV file should contain a UTF-8 BOM'); - } - - /** - * @return void - */ - public function testWriteShouldNotAddUtf8Bom() - { - $allRows = [ - ['csv--11', 'csv--12'], - ]; - $writtenContent = $this->writeToCsvFileAndReturnWrittenContent($allRows, 'csv_no_bom.csv', ',', '"', false); - - $this->assertNotContains(EncodingHelper::BOM_UTF8, $writtenContent, 'The CSV file should not contain a UTF-8 BOM'); - } - - /** - * @return void - */ - public function testWriteShouldSupportAssociativeArrays() - { - $allRows = [ - ['foo' => 'csv--11', 'bar' => 'csv--12'], - ]; - $writtenContent = $this->writeToCsvFileAndReturnWrittenContent($allRows, 'csv_from_associative_arrays.csv'); - $writtenContent = $this->trimWrittenContent($writtenContent); - - $this->assertEquals('csv--11,csv--12', $writtenContent, 'Values from associative arrays should be written'); - } - - /** - * @return void - */ - public function testWriteShouldSupportNullValues() - { - $allRows = [ - ['csv--11', null, 'csv--13'], - ]; - $writtenContent = $this->writeToCsvFileAndReturnWrittenContent($allRows, 'csv_with_null_values.csv'); - $writtenContent = $this->trimWrittenContent($writtenContent); - - $this->assertEquals('csv--11,,csv--13', $writtenContent, 'The null values should be replaced by empty values'); - } - - /** - * @return void - */ - public function testWriteShouldSkipEmptyRows() - { - $allRows = [ - ['csv--11', 'csv--12'], - [], - ['csv--31', 'csv--32'], - ]; - $writtenContent = $this->writeToCsvFileAndReturnWrittenContent($allRows, 'csv_with_empty_rows.csv'); - $writtenContent = $this->trimWrittenContent($writtenContent); - - $this->assertEquals("csv--11,csv--12\ncsv--31,csv--32", $writtenContent, 'Empty rows should be skipped'); - } - - /** - * @return void - */ - public function testWriteShouldSupportCustomFieldDelimiter() - { - $allRows = [ - ['csv--11', 'csv--12', 'csv--13'], - ['csv--21', 'csv--22', 'csv--23'], - ]; - $writtenContent = $this->writeToCsvFileAndReturnWrittenContent($allRows, 'csv_with_pipe_delimiters.csv', '|'); - $writtenContent = $this->trimWrittenContent($writtenContent); - - $this->assertEquals("csv--11|csv--12|csv--13\ncsv--21|csv--22|csv--23", $writtenContent, 'The fields should be delimited with |'); - } - - /** - * @return void - */ - public function testWriteShouldSupportCustomFieldEnclosure() - { - $allRows = [ - ['This is, a comma', 'csv--12', 'csv--13'], - ]; - $writtenContent = $this->writeToCsvFileAndReturnWrittenContent($allRows, 'csv_with_pound_enclosures.csv', ',', '#'); - $writtenContent = $this->trimWrittenContent($writtenContent); - - $this->assertEquals('#This is, a comma#,csv--12,csv--13', $writtenContent, 'The fields should be enclosed with #'); - } - - /** - * @param array $allRows - * @param string $fileName - * @param string $fieldDelimiter - * @param string $fieldEnclosure - * @param bool $shouldAddBOM - * @return null|string - */ - private function writeToCsvFileAndReturnWrittenContent($allRows, $fileName, $fieldDelimiter = ',', $fieldEnclosure = '"', $shouldAddBOM = true) - { - $this->createGeneratedFolderIfNeeded($fileName); - $resourcePath = $this->getGeneratedResourcePath($fileName); - - $writer = WriterFactory::create(Type::CSV); - $writer->setFieldDelimiter($fieldDelimiter); - $writer->setFieldEnclosure($fieldEnclosure); - $writer->setShouldAddBOM($shouldAddBOM); - - $writer->openToFile($resourcePath); - $writer->addRows($allRows); - $writer->close(); - - return file_get_contents($resourcePath); - } - - /** - * @param string $writtenContent - * @return string - */ - private function trimWrittenContent($writtenContent) - { - // remove line feeds and UTF-8 BOM - return trim($writtenContent, PHP_EOL . EncodingHelper::BOM_UTF8); - } -} diff --git a/tests/Spout/Writer/Common/Helper/CellHelperTest.php b/tests/Spout/Writer/Common/Helper/CellHelperTest.php deleted file mode 100644 index a07046f..0000000 --- a/tests/Spout/Writer/Common/Helper/CellHelperTest.php +++ /dev/null @@ -1,112 +0,0 @@ -assertEquals($expectedCellIndex, CellHelper::getCellIndexFromColumnIndex($columnIndex)); - } - - /** - * @return array - */ - public function testIsEmpty() - { - $this->assertTrue(CellHelper::isEmpty(null)); - $this->assertTrue(CellHelper::isEmpty("")); - - $this->assertFalse(CellHelper::isEmpty("string")); - $this->assertFalse(CellHelper::isEmpty(0)); - $this->assertFalse(CellHelper::isEmpty(1)); - $this->assertFalse(CellHelper::isEmpty(true)); - $this->assertFalse(CellHelper::isEmpty(false)); - $this->assertFalse(CellHelper::isEmpty(["string"])); - $this->assertFalse(CellHelper::isEmpty(new \stdClass())); - } - - /** - * @return array - */ - public function testIsNonEmptyString() - { - $this->assertTrue(CellHelper::isNonEmptyString("string")); - - $this->assertFalse(CellHelper::isNonEmptyString("")); - $this->assertFalse(CellHelper::isNonEmptyString(0)); - $this->assertFalse(CellHelper::isNonEmptyString(1)); - $this->assertFalse(CellHelper::isNonEmptyString(true)); - $this->assertFalse(CellHelper::isNonEmptyString(false)); - $this->assertFalse(CellHelper::isNonEmptyString(["string"])); - $this->assertFalse(CellHelper::isNonEmptyString(new \stdClass())); - $this->assertFalse(CellHelper::isNonEmptyString(null)); - } - - /** - * @return array - */ - public function testIsNumeric() - { - $this->assertTrue(CellHelper::isNumeric(0)); - $this->assertTrue(CellHelper::isNumeric(10)); - $this->assertTrue(CellHelper::isNumeric(10.1)); - $this->assertTrue(CellHelper::isNumeric(10.10000000000000000000001)); - $this->assertTrue(CellHelper::isNumeric(0x539)); - $this->assertTrue(CellHelper::isNumeric(02471)); - $this->assertTrue(CellHelper::isNumeric(0b10100111001)); - $this->assertTrue(CellHelper::isNumeric(1337e0)); - - $this->assertFalse(CellHelper::isNumeric("0")); - $this->assertFalse(CellHelper::isNumeric("42")); - $this->assertFalse(CellHelper::isNumeric(true)); - $this->assertFalse(CellHelper::isNumeric([2])); - $this->assertFalse(CellHelper::isNumeric(new \stdClass())); - $this->assertFalse(CellHelper::isNumeric(null)); - } - - /** - * @return array - */ - public function testIsBoolean() - { - $this->assertTrue(CellHelper::isBoolean(true)); - $this->assertTrue(CellHelper::isBoolean(false)); - - $this->assertFalse(CellHelper::isBoolean(0)); - $this->assertFalse(CellHelper::isBoolean(1)); - $this->assertFalse(CellHelper::isBoolean("0")); - $this->assertFalse(CellHelper::isBoolean("1")); - $this->assertFalse(CellHelper::isBoolean("true")); - $this->assertFalse(CellHelper::isBoolean("false")); - $this->assertFalse(CellHelper::isBoolean([true])); - $this->assertFalse(CellHelper::isBoolean(new \stdClass())); - $this->assertFalse(CellHelper::isBoolean(null)); - } -} diff --git a/tests/Spout/Writer/Common/SheetTest.php b/tests/Spout/Writer/Common/SheetTest.php deleted file mode 100644 index 032c775..0000000 --- a/tests/Spout/Writer/Common/SheetTest.php +++ /dev/null @@ -1,111 +0,0 @@ -assertEquals('Sheet1', $sheets[0]->getName(), 'Invalid name for the first sheet'); - $this->assertEquals('Sheet2', $sheets[1]->getName(), 'Invalid name for the second sheet'); - } - - /** - * @return void - */ - public function testSetSheetNameShouldCreateSheetWithCustomName() - { - $customSheetName = 'CustomName'; - $sheet = new Sheet(0, 'workbookId1'); - $sheet->setName($customSheetName); - - $this->assertEquals($customSheetName, $sheet->getName(), "The sheet name should have been changed to '$customSheetName'"); - } - - /** - * @return array - */ - public function dataProviderForInvalidSheetNames() - { - return [ - [null], - [21], - [''], - ['this title exceeds the 31 characters limit'], - ['Illegal \\'], - ['Illegal /'], - ['Illegal ?'], - ['Illegal *'], - ['Illegal :'], - ['Illegal ['], - ['Illegal ]'], - ['\'Illegal start'], - ['Illegal end\''], - ]; - } - - /** - * @dataProvider dataProviderForInvalidSheetNames - * @expectedException \Box\Spout\Writer\Exception\InvalidSheetNameException - * - * @param string $customSheetName - * @return void - */ - public function testSetSheetNameShouldThrowOnInvalidName($customSheetName) - { - (new Sheet(0, 'workbookId1'))->setName($customSheetName); - } - - /** - * @return void - */ - public function testSetSheetNameShouldNotThrowWhenSettingSameNameAsCurrentOne() - { - $customSheetName = 'Sheet name'; - $sheet = new Sheet(0, 'workbookId1'); - $sheet->setName($customSheetName); - $sheet->setName($customSheetName); - } - - /** - * @expectedException \Box\Spout\Writer\Exception\InvalidSheetNameException - * @return void - */ - public function testSetSheetNameShouldThrowWhenNameIsAlreadyUsed() - { - $customSheetName = 'Sheet name'; - - $sheet = new Sheet(0, 'workbookId1'); - $sheet->setName($customSheetName); - - $sheet = new Sheet(1, 'workbookId1'); - $sheet->setName($customSheetName); - } - - /** - * @return void - */ - public function testSetSheetNameShouldNotThrowWhenSameNameUsedInDifferentWorkbooks() - { - $customSheetName = 'Sheet name'; - - $sheet = new Sheet(0, 'workbookId1'); - $sheet->setName($customSheetName); - - $sheet = new Sheet(0, 'workbookId2'); - $sheet->setName($customSheetName); - - $sheet = new Sheet(1, 'workbookId3'); - $sheet->setName($customSheetName); - } -} diff --git a/tests/Spout/Writer/ODS/Helper/StyleHelperTest.php b/tests/Spout/Writer/ODS/Helper/StyleHelperTest.php deleted file mode 100644 index 763f904..0000000 --- a/tests/Spout/Writer/ODS/Helper/StyleHelperTest.php +++ /dev/null @@ -1,89 +0,0 @@ -defaultStyle = (new StyleBuilder())->build(); - } - - /** - * @return void - */ - public function testRegisterStyleShouldUpdateId() - { - $style1 = (new StyleBuilder())->setFontBold()->build(); - $style2 = (new StyleBuilder())->setFontUnderline()->build(); - - $this->assertEquals(0, $this->defaultStyle->getId(), 'Default style ID should be 0'); - $this->assertNull($style1->getId()); - $this->assertNull($style2->getId()); - - $styleHelper = new StyleHelper($this->defaultStyle); - $registeredStyle1 = $styleHelper->registerStyle($style1); - $registeredStyle2 = $styleHelper->registerStyle($style2); - - $this->assertEquals(1, $registeredStyle1->getId()); - $this->assertEquals(2, $registeredStyle2->getId()); - } - - /** - * @return void - */ - public function testRegisterStyleShouldReuseAlreadyRegisteredStyles() - { - $style = (new StyleBuilder())->setFontBold()->build(); - - $styleHelper = new StyleHelper($this->defaultStyle); - $registeredStyle1 = $styleHelper->registerStyle($style); - $registeredStyle2 = $styleHelper->registerStyle($style); - - $this->assertEquals(1, $registeredStyle1->getId()); - $this->assertEquals(1, $registeredStyle2->getId()); - } - - /** - * @return void - */ - public function testApplyExtraStylesIfNeededShouldApplyWrapTextIfCellContainsNewLine() - { - $style = clone $this->defaultStyle; - $styleHelper = new StyleHelper($this->defaultStyle); - - $this->assertFalse($style->shouldWrapText()); - - $updatedStyle = $styleHelper->applyExtraStylesIfNeeded($style, [12, 'single line', "multi\nlines", null]); - - $this->assertTrue($updatedStyle->shouldWrapText()); - } - - /** - * @return void - */ - public function testApplyExtraStylesIfNeededShouldDoNothingIfWrapTextAlreadyApplied() - { - $style = (new StyleBuilder())->setShouldWrapText()->build(); - $styleHelper = new StyleHelper($this->defaultStyle); - - $this->assertTrue($style->shouldWrapText()); - - $updatedStyle = $styleHelper->applyExtraStylesIfNeeded($style, ["multi\nlines"]); - - $this->assertTrue($updatedStyle->shouldWrapText()); - } -} diff --git a/tests/Spout/Writer/ODS/SheetTest.php b/tests/Spout/Writer/ODS/SheetTest.php deleted file mode 100644 index ee59501..0000000 --- a/tests/Spout/Writer/ODS/SheetTest.php +++ /dev/null @@ -1,134 +0,0 @@ -writeDataToMulitpleSheetsAndReturnSheets('test_get_sheet_index.ods'); - - $this->assertEquals(2, count($sheets), '2 sheets should have been created'); - $this->assertEquals(0, $sheets[0]->getIndex(), 'The first sheet should be index 0'); - $this->assertEquals(1, $sheets[1]->getIndex(), 'The second sheet should be index 1'); - } - - /** - * @return void - */ - public function testGetSheetName() - { - $sheets = $this->writeDataToMulitpleSheetsAndReturnSheets('test_get_sheet_name.ods'); - - $this->assertEquals(2, count($sheets), '2 sheets should have been created'); - $this->assertEquals('Sheet1', $sheets[0]->getName(), 'Invalid name for the first sheet'); - $this->assertEquals('Sheet2', $sheets[1]->getName(), 'Invalid name for the second sheet'); - } - - /** - * @return void - */ - public function testSetSheetNameShouldCreateSheetWithCustomName() - { - $fileName = 'test_set_name_should_create_sheet_with_custom_name.ods'; - $customSheetName = 'CustomName'; - $this->writeDataAndReturnSheetWithCustomName($fileName, $customSheetName); - - $this->assertSheetNameEquals($customSheetName, $fileName, "The sheet name should have been changed to '$customSheetName'"); - } - - /** - * @expectedException \Box\Spout\Writer\Exception\InvalidSheetNameException - * @return void - */ - public function testSetSheetNameShouldThrowWhenNameIsAlreadyUsed() - { - $fileName = 'test_set_name_with_non_unique_name.ods'; - $this->createGeneratedFolderIfNeeded($fileName); - $resourcePath = $this->getGeneratedResourcePath($fileName); - - $writer = WriterFactory::create(Type::ODS); - $writer->openToFile($resourcePath); - - $customSheetName = 'Sheet name'; - - $sheet = $writer->getCurrentSheet(); - $sheet->setName($customSheetName); - - $writer->addNewSheetAndMakeItCurrent(); - $sheet = $writer->getCurrentSheet(); - $sheet->setName($customSheetName); - } - - /** - * @param string $fileName - * @param string $sheetName - * @return Sheet - */ - private function writeDataAndReturnSheetWithCustomName($fileName, $sheetName) - { - $this->createGeneratedFolderIfNeeded($fileName); - $resourcePath = $this->getGeneratedResourcePath($fileName); - - $writer = WriterFactory::create(Type::ODS); - $writer->openToFile($resourcePath); - - $sheet = $writer->getCurrentSheet(); - $sheet->setName($sheetName); - - $writer->addRow(['ods--11', 'ods--12']); - $writer->close(); - } - - /** - * @param string $fileName - * @return Sheet[] - */ - private function writeDataToMulitpleSheetsAndReturnSheets($fileName) - { - $this->createGeneratedFolderIfNeeded($fileName); - $resourcePath = $this->getGeneratedResourcePath($fileName); - - /** @var \Box\Spout\Writer\ODS\Writer $writer */ - $writer = WriterFactory::create(Type::ODS); - $writer->openToFile($resourcePath); - - $writer->addRow(['ods--sheet1--11', 'ods--sheet1--12']); - $writer->addNewSheetAndMakeItCurrent(); - $writer->addRow(['ods--sheet2--11', 'ods--sheet2--12', 'ods--sheet2--13']); - - $writer->close(); - - return $writer->getSheets(); - } - - /** - * @param string $expectedName - * @param string $fileName - * @param string $message - * @return void - */ - private function assertSheetNameEquals($expectedName, $fileName, $message = '') - { - $resourcePath = $this->getGeneratedResourcePath($fileName); - $pathToWorkbookFile = $resourcePath . '#content.xml'; - $xmlContents = file_get_contents('zip://' . $pathToWorkbookFile); - - $this->assertContains("table:name=\"$expectedName\"", $xmlContents, $message); - } -} diff --git a/tests/Spout/Writer/ODS/WriterTest.php b/tests/Spout/Writer/ODS/WriterTest.php deleted file mode 100644 index 836e679..0000000 --- a/tests/Spout/Writer/ODS/WriterTest.php +++ /dev/null @@ -1,603 +0,0 @@ -createUnwritableFolderIfNeeded(); - $filePath = $this->getGeneratedUnwritableResourcePath($fileName); - - $writer = WriterFactory::create(Type::ODS); - @$writer->openToFile($filePath); - } - - /** - * @expectedException \Box\Spout\Writer\Exception\WriterNotOpenedException - */ - public function testAddRowShouldThrowExceptionIfCallAddRowBeforeOpeningWriter() - { - $writer = WriterFactory::create(Type::ODS); - $writer->addRow(['ods--11', 'ods--12']); - } - - /** - * @expectedException \Box\Spout\Writer\Exception\WriterNotOpenedException - */ - public function testAddRowShouldThrowExceptionIfCalledBeforeOpeningWriter() - { - $writer = WriterFactory::create(Type::ODS); - $writer->addRows([['ods--11', 'ods--12']]); - } - - /** - * @expectedException \Box\Spout\Writer\Exception\WriterAlreadyOpenedException - */ - public function testSetTempFolderShouldThrowExceptionIfCalledAfterOpeningWriter() - { - $fileName = 'file_that_wont_be_written.ods'; - $filePath = $this->getGeneratedResourcePath($fileName); - - /** @var \Box\Spout\Writer\ODS\Writer $writer */ - $writer = WriterFactory::create(Type::ODS); - $writer->openToFile($filePath); - - $writer->setTempFolder(''); - } - - /** - * @expectedException \Box\Spout\Writer\Exception\WriterAlreadyOpenedException - */ - public function testSetShouldCreateNewSheetsAutomaticallyShouldThrowExceptionIfCalledAfterOpeningWriter() - { - $fileName = 'file_that_wont_be_written.ods'; - $filePath = $this->getGeneratedResourcePath($fileName); - - /** @var \Box\Spout\Writer\ODS\Writer $writer */ - $writer = WriterFactory::create(Type::ODS); - $writer->openToFile($filePath); - - $writer->setShouldCreateNewSheetsAutomatically(true); - } - - /** - * @expectedException \Box\Spout\Common\Exception\InvalidArgumentException - */ - public function testAddRowShouldThrowExceptionIfUnsupportedDataTypePassedIn() - { - $fileName = 'test_add_row_should_throw_exception_if_unsupported_data_type_passed_in.ods'; - $dataRows = [ - [new \stdClass()], - ]; - - $this->writeToODSFile($dataRows, $fileName); - } - - /** - * @return void - */ - public function testAddRowShouldCleanupAllFilesIfExceptionIsThrown() - { - $fileName = 'test_add_row_should_cleanup_all_files_if_exception_thrown.ods'; - $dataRows = [ - ['wrong'], - [new \stdClass()], - ]; - - $this->createGeneratedFolderIfNeeded($fileName); - $resourcePath = $this->getGeneratedResourcePath($fileName); - - $this->recreateTempFolder(); - $tempFolderPath = $this->getTempFolderPath(); - - /** @var \Box\Spout\Writer\ODS\Writer $writer */ - $writer = WriterFactory::create(Type::ODS); - $writer->setTempFolder($tempFolderPath); - $writer->openToFile($resourcePath); - - try { - $writer->addRows($dataRows); - $this->fail('Exception should have been thrown'); - } catch (SpoutException $e) { - $this->assertFalse(file_exists($fileName), 'Output file should have been deleted'); - - $numFiles = iterator_count(new \FilesystemIterator($tempFolderPath, \FilesystemIterator::SKIP_DOTS)); - $this->assertEquals(0, $numFiles, 'All temp files should have been deleted'); - } - } - - /** - * @return void - */ - public function testAddNewSheetAndMakeItCurrent() - { - $fileName = 'test_add_new_sheet_and_make_it_current.ods'; - $this->createGeneratedFolderIfNeeded($fileName); - $resourcePath = $this->getGeneratedResourcePath($fileName); - - /** @var Writer $writer */ - $writer = WriterFactory::create(Type::ODS); - $writer->openToFile($resourcePath); - $writer->addNewSheetAndMakeItCurrent(); - $writer->close(); - - $sheets = $writer->getSheets(); - $this->assertEquals(2, count($sheets), 'There should be 2 sheets'); - $this->assertEquals($sheets[1], $writer->getCurrentSheet(), 'The current sheet should be the second one.'); - } - - /** - * @return void - */ - public function testSetCurrentSheet() - { - $fileName = 'test_set_current_sheet.ods'; - $this->createGeneratedFolderIfNeeded($fileName); - $resourcePath = $this->getGeneratedResourcePath($fileName); - - $writer = WriterFactory::create(Type::ODS); - $writer->openToFile($resourcePath); - - $writer->addNewSheetAndMakeItCurrent(); - $writer->addNewSheetAndMakeItCurrent(); - - $firstSheet = $writer->getSheets()[0]; - $writer->setCurrentSheet($firstSheet); - - $writer->close(); - - $this->assertEquals($firstSheet, $writer->getCurrentSheet(), 'The current sheet should be the first one.'); - } - - /** - * @return void - */ - public function testCloseShouldNoopWhenWriterIsNotOpened() - { - $fileName = 'test_double_close_calls.ods'; - $this->createGeneratedFolderIfNeeded($fileName); - $resourcePath = $this->getGeneratedResourcePath($fileName); - - $writer = WriterFactory::create(Type::ODS); - $writer->close(); // This call should not cause any error - - $writer->openToFile($resourcePath); - $writer->close(); - $writer->close(); // This call should not cause any error - } - - /** - * @return void - */ - public function testAddRowShouldWriteGivenDataToSheet() - { - $fileName = 'test_add_row_should_write_given_data_to_sheet.ods'; - $dataRows = [ - ['ods--11', 'ods--12'], - ['ods--21', 'ods--22', 'ods--23'], - ]; - - $this->writeToODSFile($dataRows, $fileName); - - foreach ($dataRows as $dataRow) { - foreach ($dataRow as $cellValue) { - $this->assertValueWasWritten($fileName, $cellValue); - } - } - } - - /** - * @return void - */ - public function testAddRowShouldWriteGivenDataToTwoSheets() - { - $fileName = 'test_add_row_should_write_given_data_to_two_sheets.ods'; - $dataRows = [ - ['ods--11', 'ods--12'], - ['ods--21', 'ods--22', 'ods--23'], - ]; - - $numSheets = 2; - $this->writeToMultipleSheetsInODSFile($dataRows, $numSheets, $fileName); - - for ($i = 1; $i <= $numSheets; $i++) { - foreach ($dataRows as $dataRow) { - foreach ($dataRow as $cellValue) { - $this->assertValueWasWritten($fileName, $cellValue); - } - } - } - } - - /** - * @return void - */ - public function testAddRowShouldSupportAssociativeArrays() - { - $fileName = 'test_add_row_should_support_associative_arrays.ods'; - $dataRows = [ - ['foo' => 'ods--11', 'bar' => 'ods--12'], - ]; - - $this->writeToODSFile($dataRows, $fileName); - - foreach ($dataRows as $dataRow) { - foreach ($dataRow as $cellValue) { - $this->assertValueWasWritten($fileName, $cellValue); - } - } - } - - /** - * @return void - */ - public function testAddRowShouldSupportMultipleTypesOfData() - { - $fileName = 'test_add_row_should_support_multiple_types_of_data.ods'; - $dataRows = [ - ['ods--11', true, '', 0, 10.2, null], - ]; - - $this->writeToODSFile($dataRows, $fileName); - - $this->assertValueWasWritten($fileName, 'ods--11'); - $this->assertValueWasWrittenToSheet($fileName, 1, 1); // true is converted to 1 - $this->assertValueWasWrittenToSheet($fileName, 1, 0); - $this->assertValueWasWrittenToSheet($fileName, 1, 10.2); - } - - /** - * @return array - */ - public function dataProviderForTestAddRowShouldUseNumberColumnsRepeatedForRepeatedValues() - { - return [ - [['ods--11', 'ods--11', 'ods--11'], 1, 3], - [['', ''], 1, 2], - [[true, true, true, true], 1, 4], - [[1.1, 1.1], 1, 2], - [['foo', 'bar'], 2, 0], - ]; - } - /** - * @dataProvider dataProviderForTestAddRowShouldUseNumberColumnsRepeatedForRepeatedValues - * - * @param array $dataRow - * @param int $expectedNumTableCells - * @param int $expectedNumColumnsRepeated - * @return void - */ - public function testAddRowShouldUseNumberColumnsRepeatedForRepeatedValues($dataRow, $expectedNumTableCells, $expectedNumColumnsRepeated) - { - $fileName = 'test_add_row_should_use_number_columns_repeated.ods'; - $this->writeToODSFile([$dataRow], $fileName); - - $sheetXmlNode = $this->getSheetXmlNode($fileName, 1); - $tableCellNodes = $sheetXmlNode->getElementsByTagName('table-cell'); - - $this->assertEquals($expectedNumTableCells, $tableCellNodes->length); - - if ($expectedNumTableCells === 1) { - $tableCellNode = $tableCellNodes->item(0); - $numColumnsRepeated = intval($tableCellNode->getAttribute('table:number-columns-repeated')); - $this->assertEquals($expectedNumColumnsRepeated, $numColumnsRepeated); - } else { - foreach ($tableCellNodes as $tableCellNode) { - $this->assertFalse($tableCellNode->hasAttribute('table:number-columns-repeated')); - } - } - } - - /** - * @return void - */ - public function testAddRowShouldWriteGivenDataToTheCorrectSheet() - { - $fileName = 'test_add_row_should_write_given_data_to_the_correct_sheet.ods'; - $dataRowsSheet1 = [ - ['ods--sheet1--11', 'ods--sheet1--12'], - ['ods--sheet1--21', 'ods--sheet1--22', 'ods--sheet1--23'], - ]; - $dataRowsSheet2 = [ - ['ods--sheet2--11', 'ods--sheet2--12'], - ['ods--sheet2--21', 'ods--sheet2--22', 'ods--sheet2--23'], - ]; - $dataRowsSheet1Again = [ - ['ods--sheet1--31', 'ods--sheet1--32'], - ['ods--sheet1--41', 'ods--sheet1--42', 'ods--sheet1--43'], - ]; - - $this->createGeneratedFolderIfNeeded($fileName); - $resourcePath = $this->getGeneratedResourcePath($fileName); - - /** @var \Box\Spout\Writer\ODS\Writer $writer */ - $writer = WriterFactory::create(Type::ODS); - $writer->openToFile($resourcePath); - - $writer->addRows($dataRowsSheet1); - - $writer->addNewSheetAndMakeItCurrent(); - $writer->addRows($dataRowsSheet2); - - $firstSheet = $writer->getSheets()[0]; - $writer->setCurrentSheet($firstSheet); - - $writer->addRows($dataRowsSheet1Again); - - $writer->close(); - - foreach ($dataRowsSheet1 as $dataRow) { - foreach ($dataRow as $cellValue) { - $this->assertValueWasWrittenToSheet($fileName, 1, $cellValue, 'Data should have been written in Sheet 1'); - } - } - foreach ($dataRowsSheet2 as $dataRow) { - foreach ($dataRow as $cellValue) { - $this->assertValueWasWrittenToSheet($fileName, 2, $cellValue, 'Data should have been written in Sheet 2'); - } - } - foreach ($dataRowsSheet1Again as $dataRow) { - foreach ($dataRow as $cellValue) { - $this->assertValueWasWrittenToSheet($fileName, 1, $cellValue, 'Data should have been written in Sheet 1'); - } - } - } - - /** - * @return void - */ - public function testAddRowShouldAutomaticallyCreateNewSheetsIfMaxRowsReachedAndOptionTurnedOn() - { - $fileName = 'test_add_row_should_automatically_create_new_sheets_if_max_rows_reached_and_option_turned_on.ods'; - $dataRows = [ - ['ods--sheet1--11', 'ods--sheet1--12'], - ['ods--sheet1--21', 'ods--sheet1--22', 'ods--sheet1--23'], - ['ods--sheet2--11', 'ods--sheet2--12'], // this should be written in a new sheet - ]; - - // set the maxRowsPerSheet limit to 2 - \ReflectionHelper::setStaticValue('\Box\Spout\Writer\ODS\Internal\Workbook', 'maxRowsPerWorksheet', 2); - - $writer = $this->writeToODSFile($dataRows, $fileName, $shouldCreateSheetsAutomatically = true); - $this->assertEquals(2, count($writer->getSheets()), '2 sheets should have been created.'); - - $this->assertValueWasNotWrittenToSheet($fileName, 1, 'ods--sheet2--11'); - $this->assertValueWasWrittenToSheet($fileName, 2, 'ods--sheet2--11'); - - \ReflectionHelper::reset(); - } - - /** - * @return void - */ - public function testAddRowShouldNotCreateNewSheetsIfMaxRowsReachedAndOptionTurnedOff() - { - $fileName = 'test_add_row_should_not_create_new_sheets_if_max_rows_reached_and_option_turned_off.ods'; - $dataRows = [ - ['ods--sheet1--11', 'ods--sheet1--12'], - ['ods--sheet1--21', 'ods--sheet1--22', 'ods--sheet1--23'], - ['ods--sheet1--31', 'ods--sheet1--32'], // this should NOT be written in a new sheet - ]; - - // set the maxRowsPerSheet limit to 2 - \ReflectionHelper::setStaticValue('\Box\Spout\Writer\ODS\Internal\Workbook', 'maxRowsPerWorksheet', 2); - - $writer = $this->writeToODSFile($dataRows, $fileName, $shouldCreateSheetsAutomatically = false); - $this->assertEquals(1, count($writer->getSheets()), 'Only 1 sheet should have been created.'); - - $this->assertValueWasNotWrittenToSheet($fileName, 1, 'ods--sheet1--31'); - - \ReflectionHelper::reset(); - } - - /** - * @return void - */ - public function testAddRowShouldEscapeHtmlSpecialCharacters() - { - $fileName = 'test_add_row_should_escape_html_special_characters.ods'; - $dataRows = [ - ['I\'m in "great" mood', 'This be escaped & tested'], - ]; - - $this->writeToODSFile($dataRows, $fileName); - - $this->assertValueWasWritten($fileName, 'I\'m in "great" mood', 'Quotes should not be escaped'); - $this->assertValueWasWritten($fileName, 'This <must> be escaped & tested', '<, > and & should be escaped'); - } - - /** - * @return void - */ - public function testAddRowShouldKeepNewLines() - { - $fileName = 'test_add_row_should_keep_new_lines.ods'; - $dataRow = ["I have\na dream"]; - - $this->writeToODSFile([$dataRow], $fileName); - - $this->assertValueWasWrittenToSheet($fileName, 1, 'I have'); - $this->assertValueWasWrittenToSheet($fileName, 1, 'a dream'); - } - - /** - * @return void - */ - public function testGeneratedFileShouldHaveTheCorrectMimeType() - { - // Only PHP7+ gives the correct mime type since it requires adding - // uncompressed files to the final archive (which support was added in PHP7) - if (!ZipHelper::canChooseCompressionMethod()) { - $this->markTestSkipped( - 'The PHP version used does not support setting the compression method of archived files, - resulting in the mime type to be detected incorrectly.' - ); - } - - $fileName = 'test_mime_type.ods'; - $resourcePath = $this->getGeneratedResourcePath($fileName); - $dataRow = ['foo']; - - $this->writeToODSFile([$dataRow], $fileName); - - $finfo = new \finfo(FILEINFO_MIME_TYPE); - $this->assertEquals('application/vnd.oasis.opendocument.spreadsheet', $finfo->file($resourcePath)); - } - - /** - * @param array $allRows - * @param string $fileName - * @param bool $shouldCreateSheetsAutomatically - * @return Writer - */ - private function writeToODSFile($allRows, $fileName, $shouldCreateSheetsAutomatically = true) - { - $this->createGeneratedFolderIfNeeded($fileName); - $resourcePath = $this->getGeneratedResourcePath($fileName); - - /** @var \Box\Spout\Writer\ODS\Writer $writer */ - $writer = WriterFactory::create(Type::ODS); - $writer->setShouldCreateNewSheetsAutomatically($shouldCreateSheetsAutomatically); - - $writer->openToFile($resourcePath); - $writer->addRows($allRows); - $writer->close(); - - return $writer; - } - - /** - * @param array $allRows - * @param int $numSheets - * @param string $fileName - * @param bool $shouldCreateSheetsAutomatically - * @return Writer - */ - private function writeToMultipleSheetsInODSFile($allRows, $numSheets, $fileName, $shouldCreateSheetsAutomatically = true) - { - $this->createGeneratedFolderIfNeeded($fileName); - $resourcePath = $this->getGeneratedResourcePath($fileName); - - /** @var \Box\Spout\Writer\ODS\Writer $writer */ - $writer = WriterFactory::create(Type::ODS); - $writer->setShouldCreateNewSheetsAutomatically($shouldCreateSheetsAutomatically); - - $writer->openToFile($resourcePath); - $writer->addRows($allRows); - - for ($i = 1; $i < $numSheets; $i++) { - $writer->addNewSheetAndMakeItCurrent(); - $writer->addRows($allRows); - } - - $writer->close(); - - return $writer; - } - - /** - * @param string $fileName - * @param string $value - * @param string $message - * @return void - */ - private function assertValueWasWritten($fileName, $value, $message = '') - { - $resourcePath = $this->getGeneratedResourcePath($fileName); - $pathToContentFile = $resourcePath . '#content.xml'; - $xmlContents = file_get_contents('zip://' . $pathToContentFile); - - $this->assertContains($value, $xmlContents, $message); - } - - /** - * @param string $fileName - * @param int $sheetIndex - * @param mixed $value - * @param string $message - * @return void - */ - private function assertValueWasWrittenToSheet($fileName, $sheetIndex, $value, $message = '') - { - $sheetXmlAsString = $this->getSheetXmlNodeAsString($fileName, $sheetIndex); - $valueAsXmlString = "$value"; - - $this->assertContains($valueAsXmlString, $sheetXmlAsString, $message); - } - - /** - * @param string $fileName - * @param int $sheetIndex - * @param mixed $value - * @param string $message - * @return void - */ - private function assertValueWasNotWrittenToSheet($fileName, $sheetIndex, $value, $message = '') - { - $sheetXmlAsString = $this->getSheetXmlNodeAsString($fileName, $sheetIndex); - $valueAsXmlString = "$value"; - - $this->assertNotContains($valueAsXmlString, $sheetXmlAsString, $message); - } - - /** - * @param string $fileName - * @param int $sheetIndex - * @return \DOMNode - */ - private function getSheetXmlNode($fileName, $sheetIndex) - { - $xmlReader = $this->moveReaderToCorrectTableNode($fileName, $sheetIndex); - return $xmlReader->expand(); - } - - /** - * @param string $fileName - * @param int $sheetIndex - * @return string - */ - private function getSheetXmlNodeAsString($fileName, $sheetIndex) - { - $xmlReader = $this->moveReaderToCorrectTableNode($fileName, $sheetIndex); - return $xmlReader->readOuterXml(); - } - - /** - * @param string $fileName - * @param int $sheetIndex - * @return XMLReader - */ - private function moveReaderToCorrectTableNode($fileName, $sheetIndex) - { - $resourcePath = $this->getGeneratedResourcePath($fileName); - - $xmlReader = new XMLReader(); - $xmlReader->openFileInZip($resourcePath, 'content.xml'); - $xmlReader->readUntilNodeFound('table:table'); - - for ($i = 1; $i < $sheetIndex; $i++) { - $xmlReader->readUntilNodeFound('table:table'); - } - - return $xmlReader; - } -} diff --git a/tests/Spout/Writer/ODS/WriterWithStyleTest.php b/tests/Spout/Writer/ODS/WriterWithStyleTest.php deleted file mode 100644 index f6af4a1..0000000 --- a/tests/Spout/Writer/ODS/WriterWithStyleTest.php +++ /dev/null @@ -1,494 +0,0 @@ -defaultStyle = (new StyleBuilder())->build(); - } - - /** - * @expectedException \Box\Spout\Writer\Exception\WriterNotOpenedException - */ - public function testAddRowWithStyleShouldThrowExceptionIfCallAddRowBeforeOpeningWriter() - { - $writer = WriterFactory::create(Type::ODS); - $writer->addRowWithStyle(['ods--11', 'ods--12'], $this->defaultStyle); - } - - /** - * @expectedException \Box\Spout\Writer\Exception\WriterNotOpenedException - */ - public function testAddRowWithStyleShouldThrowExceptionIfCalledBeforeOpeningWriter() - { - $writer = WriterFactory::create(Type::ODS); - $writer->addRowWithStyle(['ods--11', 'ods--12'], $this->defaultStyle); - } - - /** - * @return array - */ - public function dataProviderForInvalidStyle() - { - return [ - ['style'], - [new \stdClass()], - [null], - ]; - } - - /** - * @dataProvider dataProviderForInvalidStyle - * @expectedException \Box\Spout\Common\Exception\InvalidArgumentException - * - * @param \Box\Spout\Writer\Style\Style $style - */ - public function testAddRowWithStyleShouldThrowExceptionIfInvalidStyleGiven($style) - { - $fileName = 'test_add_row_with_style_should_throw_exception.ods'; - $this->createGeneratedFolderIfNeeded($fileName); - $resourcePath = $this->getGeneratedResourcePath($fileName); - - $writer = WriterFactory::create(Type::ODS); - $writer->openToFile($resourcePath); - $writer->addRowWithStyle(['ods--11', 'ods--12'], $style); - } - - /** - * @dataProvider dataProviderForInvalidStyle - * @expectedException \Box\Spout\Common\Exception\InvalidArgumentException - * - * @param \Box\Spout\Writer\Style\Style $style - */ - public function testAddRowsWithStyleShouldThrowExceptionIfInvalidStyleGiven($style) - { - $fileName = 'test_add_row_with_style_should_throw_exception.ods'; - $this->createGeneratedFolderIfNeeded($fileName); - $resourcePath = $this->getGeneratedResourcePath($fileName); - - $writer = WriterFactory::create(Type::ODS); - $writer->openToFile($resourcePath); - $writer->addRowsWithStyle([['ods--11', 'ods--12']], $style); - } - - /** - * @return void - */ - public function testAddRowWithStyleShouldListAllUsedStylesInCreatedContentXmlFile() - { - $fileName = 'test_add_row_with_style_should_list_all_used_fonts.ods'; - $dataRows = [ - ['ods--11', 'ods--12'], - ['ods--21', 'ods--22'], - ]; - - $style = (new StyleBuilder()) - ->setFontBold() - ->setFontItalic() - ->setFontUnderline() - ->setFontStrikethrough() - ->build(); - $style2 = (new StyleBuilder()) - ->setFontSize(15) - ->setFontColor(Color::RED) - ->setFontName('Cambria') - ->setBackgroundColor(Color::GREEN) - ->build(); - - $this->writeToODSFileWithMultipleStyles($dataRows, $fileName, [$style, $style2]); - - $cellStyleElements = $this->getCellStyleElementsFromContentXmlFile($fileName); - $this->assertEquals(3, count($cellStyleElements), 'There should be 3 separate cell styles, including the default one.'); - - // Second font should contain data from the first created style - $customFont1Element = $cellStyleElements[1]; - $this->assertFirstChildHasAttributeEquals('bold', $customFont1Element, 'text-properties', 'fo:font-weight'); - $this->assertFirstChildHasAttributeEquals('italic', $customFont1Element, 'text-properties', 'fo:font-style'); - $this->assertFirstChildHasAttributeEquals('solid', $customFont1Element, 'text-properties', 'style:text-underline-style'); - $this->assertFirstChildHasAttributeEquals('solid', $customFont1Element, 'text-properties', 'style:text-line-through-style'); - - // Third font should contain data from the second created style - $customFont2Element = $cellStyleElements[2]; - $this->assertFirstChildHasAttributeEquals('15pt', $customFont2Element, 'text-properties', 'fo:font-size'); - $this->assertFirstChildHasAttributeEquals('#' . Color::RED, $customFont2Element, 'text-properties', 'fo:color'); - $this->assertFirstChildHasAttributeEquals('Cambria', $customFont2Element, 'text-properties', 'style:font-name'); - $this->assertFirstChildHasAttributeEquals('#' . Color::GREEN, $customFont2Element, 'table-cell-properties', 'fo:background-color'); - } - - /** - * @return void - */ - public function testAddRowWithStyleShouldWriteDefaultStyleSettings() - { - $fileName = 'test_add_row_with_style_should_write_default_style_settings.ods'; - $dataRow = ['ods--11', 'ods--12']; - - $this->writeToODSFile([$dataRow], $fileName, $this->defaultStyle); - - $textPropertiesElement = $this->getXmlSectionFromStylesXmlFile($fileName, 'style:text-properties'); - $this->assertEquals(Style::DEFAULT_FONT_SIZE . 'pt', $textPropertiesElement->getAttribute('fo:font-size')); - $this->assertEquals('#' . Style::DEFAULT_FONT_COLOR, $textPropertiesElement->getAttribute('fo:color')); - $this->assertEquals(Style::DEFAULT_FONT_NAME, $textPropertiesElement->getAttribute('style:font-name')); - } - - /** - * @return void - */ - public function testAddRowWithStyleShouldApplyStyleToCells() - { - $fileName = 'test_add_row_with_style_should_apply_style_to_cells.ods'; - $dataRows = [ - ['ods--11'], - ['ods--21'], - ['ods--31'], - ]; - $style = (new StyleBuilder())->setFontBold()->build(); - $style2 = (new StyleBuilder())->setFontSize(15)->build(); - - $this->writeToODSFileWithMultipleStyles($dataRows, $fileName, [$style, $style2, null]); - - $cellDomElements = $this->getCellElementsFromContentXmlFile($fileName); - $this->assertEquals(3, count($cellDomElements), 'There should be 3 cells with content'); - - $this->assertEquals('ce2', $cellDomElements[0]->getAttribute('table:style-name')); - $this->assertEquals('ce3', $cellDomElements[1]->getAttribute('table:style-name')); - $this->assertEquals('ce1', $cellDomElements[2]->getAttribute('table:style-name')); - } - - /** - * @return void - */ - public function testAddRowWithStyleShouldReuseDuplicateStyles() - { - $fileName = 'test_add_row_with_style_should_reuse_duplicate_styles.ods'; - $dataRows = [ - ['ods--11'], - ['ods--21'], - ]; - $style = (new StyleBuilder())->setFontBold()->build(); - - $this->writeToODSFile($dataRows, $fileName, $style); - - $cellDomElements = $this->getCellElementsFromContentXmlFile($fileName); - $this->assertEquals(2, count($cellDomElements), 'There should be 2 cells with content'); - - $this->assertEquals('ce2', $cellDomElements[0]->getAttribute('table:style-name')); - $this->assertEquals('ce2', $cellDomElements[1]->getAttribute('table:style-name')); - } - - /** - * @return void - */ - public function testAddRowWithStyleShouldAddWrapTextAlignmentInfoInStylesXmlFileIfSpecified() - { - $fileName = 'test_add_row_with_style_should_add_wrap_text_alignment.ods'; - $dataRows = [ - ['ods--11', 'ods--12'], - ]; - $style = (new StyleBuilder())->setShouldWrapText()->build(); - - $this->writeToODSFile($dataRows, $fileName, $style); - - $styleElements = $this->getCellStyleElementsFromContentXmlFile($fileName); - $this->assertEquals(2, count($styleElements), 'There should be 2 styles (default and custom)'); - - $customStyleElement = $styleElements[1]; - $this->assertFirstChildHasAttributeEquals('wrap', $customStyleElement, 'table-cell-properties', 'fo:wrap-option'); - } - - /** - * @return void - */ - public function testAddRowWithStyleShouldApplyWrapTextIfCellContainsNewLine() - { - $fileName = 'test_add_row_with_style_should_apply_wrap_text_if_new_lines.ods'; - $dataRows = [ - ["ods--11\nods--11"], - ]; - - $this->writeToODSFile($dataRows, $fileName, $this->defaultStyle); - - $styleElements = $this->getCellStyleElementsFromContentXmlFile($fileName); - $this->assertEquals(2, count($styleElements), 'There should be 2 styles (default and custom)'); - - $customStyleElement = $styleElements[1]; - $this->assertFirstChildHasAttributeEquals('wrap', $customStyleElement, 'table-cell-properties', 'fo:wrap-option'); - } - - /** - * @return void - */ - public function testAddBackgroundColor() - { - $fileName = 'test_default_background_style.ods'; - $dataRows = [ - ['defaultBgColor'], - ]; - - $style = (new StyleBuilder())->setBackgroundColor(Color::WHITE)->build(); - $this->writeToODSFile($dataRows, $fileName, $style); - - $styleElements = $this->getCellStyleElementsFromContentXmlFile($fileName); - $this->assertEquals(2, count($styleElements), 'There should be 2 styles (default and custom)'); - - $customStyleElement = $styleElements[1]; - $this->assertFirstChildHasAttributeEquals('#' . Color::WHITE, $customStyleElement, 'table-cell-properties', 'fo:background-color'); - } - - /** - * @return void - */ - public function testBorders() - { - $fileName = 'test_borders.ods'; - - $dataRows = [ - ['row-with-border-bottom-green-thick-solid'], - ['row-without-border'], - ['row-with-border-top-red-thin-dashed'], - ]; - - $borderBottomGreenThickSolid = (new BorderBuilder()) - ->setBorderBottom(Color::GREEN, Border::WIDTH_THICK, Border::STYLE_SOLID)->build(); - - - $borderTopRedThinDashed = (new BorderBuilder()) - ->setBorderTop(Color::RED, Border::WIDTH_THIN, Border::STYLE_DASHED)->build(); - - $styles = [ - (new StyleBuilder())->setBorder($borderBottomGreenThickSolid)->build(), - (new StyleBuilder())->build(), - (new StyleBuilder())->setBorder($borderTopRedThinDashed)->build(), - ]; - - $this->writeToODSFileWithMultipleStyles($dataRows, $fileName, $styles); - - $styleElements = $this->getCellStyleElementsFromContentXmlFile($fileName); - - $this->assertEquals(3, count($styleElements), 'There should be 3 styles)'); - - // Use reflection for protected members here - $widthMap = \ReflectionHelper::getStaticValue('Box\Spout\Writer\ODS\Helper\BorderHelper', 'widthMap'); - $styleMap = \ReflectionHelper::getStaticValue('Box\Spout\Writer\ODS\Helper\BorderHelper', 'styleMap'); - - $expectedFirst = sprintf( - '%s %s #%s', - $widthMap[Border::WIDTH_THICK], - $styleMap[Border::STYLE_SOLID], - Color::GREEN - ); - - $actualFirst = $styleElements[1] - ->getElementsByTagName('table-cell-properties') - ->item(0) - ->getAttribute('fo:border-bottom'); - - $this->assertEquals($expectedFirst, $actualFirst); - - $expectedThird = sprintf( - '%s %s #%s', - $widthMap[Border::WIDTH_THIN], - $styleMap[Border::STYLE_DASHED], - Color::RED - ); - - $actualThird = $styleElements[2] - ->getElementsByTagName('table-cell-properties') - ->item(0) - ->getAttribute('fo:border-top'); - - $this->assertEquals($expectedThird, $actualThird); - } - - /** - * @return void - */ - public function testSetDefaultRowStyle() - { - $fileName = 'test_set_default_row_style.ods'; - $dataRows = [['ods--11']]; - - $defaultFontSize = 50; - $defaultStyle = (new StyleBuilder())->setFontSize($defaultFontSize)->build(); - - $this->writeToODSFileWithDefaultStyle($dataRows, $fileName, $defaultStyle); - - $textPropertiesElement = $this->getXmlSectionFromStylesXmlFile($fileName, 'style:text-properties'); - $this->assertEquals($defaultFontSize . 'pt', $textPropertiesElement->getAttribute('fo:font-size')); - } - - /** - * @param array $allRows - * @param string $fileName - * @param \Box\Spout\Writer\Style\Style $style - * @return Writer - */ - private function writeToODSFile($allRows, $fileName, $style) - { - $this->createGeneratedFolderIfNeeded($fileName); - $resourcePath = $this->getGeneratedResourcePath($fileName); - - /** @var \Box\Spout\Writer\ODS\Writer $writer */ - $writer = WriterFactory::create(Type::ODS); - - $writer->openToFile($resourcePath); - $writer->addRowsWithStyle($allRows, $style); - $writer->close(); - - return $writer; - } - - /** - * @param array $allRows - * @param string $fileName - * @param \Box\Spout\Writer\Style\Style|null $defaultStyle - * @return Writer - */ - private function writeToODSFileWithDefaultStyle($allRows, $fileName, $defaultStyle) - { - $this->createGeneratedFolderIfNeeded($fileName); - $resourcePath = $this->getGeneratedResourcePath($fileName); - - /** @var \Box\Spout\Writer\XLSX\Writer $writer */ - $writer = WriterFactory::create(Type::ODS); - $writer->setDefaultRowStyle($defaultStyle); - - $writer->openToFile($resourcePath); - $writer->addRows($allRows); - $writer->close(); - - return $writer; - } - - /** - * @param array $allRows - * @param string $fileName - * @param \Box\Spout\Writer\Style\Style|null[] $styles - * @return Writer - */ - private function writeToODSFileWithMultipleStyles($allRows, $fileName, $styles) - { - // there should be as many rows as there are styles passed in - $this->assertEquals(count($allRows), count($styles)); - - $this->createGeneratedFolderIfNeeded($fileName); - $resourcePath = $this->getGeneratedResourcePath($fileName); - - /** @var \Box\Spout\Writer\ODS\Writer $writer */ - $writer = WriterFactory::create(Type::ODS); - - $writer->openToFile($resourcePath); - for ($i = 0; $i < count($allRows); $i++) { - if ($styles[$i] === null) { - $writer->addRow($allRows[$i]); - } else { - $writer->addRowWithStyle($allRows[$i], $styles[$i]); - } - } - $writer->close(); - - return $writer; - } - - /** - * @param string $fileName - * @return \DOMNode[] - */ - private function getCellElementsFromContentXmlFile($fileName) - { - $cellElements = []; - - $resourcePath = $this->getGeneratedResourcePath($fileName); - - $xmlReader = new XMLReader(); - $xmlReader->openFileInZip($resourcePath, 'content.xml'); - - while ($xmlReader->read()) { - if ($xmlReader->isPositionedOnStartingNode('table:table-cell') && $xmlReader->getAttribute('office:value-type') !== null) { - $cellElements[] = $xmlReader->expand(); - } - } - - $xmlReader->close(); - - return $cellElements; - } - - /** - * @param string $fileName - * @return \DOMNode[] - */ - private function getCellStyleElementsFromContentXmlFile($fileName) - { - $cellStyleElements = []; - - $resourcePath = $this->getGeneratedResourcePath($fileName); - - $xmlReader = new XMLReader(); - $xmlReader->openFileInZip($resourcePath, 'content.xml'); - - while ($xmlReader->read()) { - if ($xmlReader->isPositionedOnStartingNode('style:style') && $xmlReader->getAttribute('style:family') === 'table-cell') { - $cellStyleElements[] = $xmlReader->expand(); - } - } - - $xmlReader->close(); - - return $cellStyleElements; - } - - /** - * @param string $fileName - * @param string $section - * @return \DomElement - */ - private function getXmlSectionFromStylesXmlFile($fileName, $section) - { - $resourcePath = $this->getGeneratedResourcePath($fileName); - - $xmlReader = new XMLReader(); - $xmlReader->openFileInZip($resourcePath, 'styles.xml'); - $xmlReader->readUntilNodeFound($section); - - return $xmlReader->expand(); - } - - /** - * @param string $expectedValue - * @param \DOMNode $parentElement - * @param string $childTagName - * @param string $attributeName - * @return void - */ - private function assertFirstChildHasAttributeEquals($expectedValue, $parentElement, $childTagName, $attributeName) - { - $this->assertEquals($expectedValue, $parentElement->getElementsByTagName($childTagName)->item(0)->getAttribute($attributeName)); - } -} diff --git a/tests/Spout/Writer/Style/BorderTest.php b/tests/Spout/Writer/Style/BorderTest.php deleted file mode 100644 index 181d8cf..0000000 --- a/tests/Spout/Writer/Style/BorderTest.php +++ /dev/null @@ -1,110 +0,0 @@ -addPart(new BorderPart(Border::LEFT)) - ->addPart(new BorderPart(Border::RIGHT)) - ->addPart(new BorderPart(Border::TOP)) - ->addPart(new BorderPart(Border::BOTTOM)) - ->addPart(new BorderPart(Border::LEFT)); - - $this->assertEquals(4, count($border->getParts()), 'There should never be more than 4 border parts'); - } - - /** - * @return void - */ - public function testSetParts() - { - $border = new Border(); - $border->setParts([ - new BorderPart(Border::LEFT) - ]); - - $this->assertEquals(1, count($border->getParts()), 'It should be possible to set the border parts'); - } - - /** - * @return void - */ - public function testBorderBuilderFluent() - { - $border = (new BorderBuilder()) - ->setBorderBottom() - ->setBorderTop() - ->setBorderLeft() - ->setBorderRight() - ->build(); - $this->assertEquals(4, count($border->getParts()), 'The border builder exposes a fluent interface'); - } - - /** - * :D :S - * @return void - */ - public function testAnyCombinationOfAllowedBorderPartsParams() - { - $color = Color::BLACK; - foreach (BorderPart::getAllowedNames() as $allowedName) { - foreach (BorderPart::getAllowedStyles() as $allowedStyle) { - foreach (BorderPart::getAllowedWidths() as $allowedWidth) { - $borderPart = new BorderPart($allowedName, $color, $allowedWidth, $allowedStyle); - $border = new Border(); - $border->addPart($borderPart); - $this->assertEquals(1, count($border->getParts())); - - /** @var $part BorderPart */ - $part = $border->getParts()[$allowedName]; - - $this->assertEquals($allowedStyle, $part->getStyle()); - $this->assertEquals($allowedWidth, $part->getWidth()); - $this->assertEquals($color, $part->getColor()); - } - } - } - } -} diff --git a/tests/Spout/Writer/Style/ColorTest.php b/tests/Spout/Writer/Style/ColorTest.php deleted file mode 100644 index 7dd4587..0000000 --- a/tests/Spout/Writer/Style/ColorTest.php +++ /dev/null @@ -1,93 +0,0 @@ -assertEquals($expectedColor, $color); - } - - /** - * @return array - */ - public function dataProviderForTestRGBAInvalidColorComponents() - { - return [ - [-1, 0, 0], - [0, -1, 0], - [0, 0, -1], - [999, 0, 0], - [0, 999, 0], - [0, 0, 999], - [null, 0, 0], - [0, null, 0], - [0, 0, null], - ['1', 0, 0], - [0, '1', 0], - [0, 0, '1'], - [true, 0, 0], - [0, true, 0], - [0, 0, true], - ]; - } - - /** - * @dataProvider dataProviderForTestRGBAInvalidColorComponents - * @expectedException \Box\Spout\Writer\Exception\InvalidColorException - * - * @param int $red - * @param int $green - * @param int $blue - * @return void - */ - public function testRGBInvalidColorComponents($red, $green, $blue) - { - Color::rgb($red, $green, $blue); - } -} diff --git a/tests/Spout/Writer/Style/StyleTest.php b/tests/Spout/Writer/Style/StyleTest.php deleted file mode 100644 index 7d3ec36..0000000 --- a/tests/Spout/Writer/Style/StyleTest.php +++ /dev/null @@ -1,160 +0,0 @@ -setFontBold()->build(); - $style1->setId(1); - - $style2 = (new StyleBuilder())->setFontBold()->build(); - $style2->setId(2); - - $this->assertEquals($style1->serialize(), $style2->serialize()); - } - - /** - * @return void - */ - public function testMergeWithShouldReturnACopy() - { - $baseStyle = (new StyleBuilder())->build(); - $currentStyle = (new StyleBuilder())->build(); - $mergedStyle = $currentStyle->mergeWith($baseStyle); - - $this->assertNotSame($mergedStyle, $currentStyle); - } - - /** - * @return void - */ - public function testMergeWithShouldMergeSetProperties() - { - $baseStyle = (new StyleBuilder())->setFontSize(99)->setFontBold()->build(); - $currentStyle = (new StyleBuilder())->setFontName('Font')->setFontUnderline()->build(); - $mergedStyle = $currentStyle->mergeWith($baseStyle); - - $this->assertNotEquals(99, $currentStyle->getFontSize()); - $this->assertFalse($currentStyle->isFontBold()); - - $this->assertEquals(99, $mergedStyle->getFontSize()); - $this->assertTrue($mergedStyle->isFontBold()); - $this->assertEquals('Font', $mergedStyle->getFontName()); - $this->assertTrue($mergedStyle->isFontUnderline()); - } - - /** - * @return void - */ - public function testMergeWithShouldPreferCurrentStylePropertyIfSetOnCurrentAndOnBase() - { - $baseStyle = (new StyleBuilder())->setFontSize(10)->build(); - $currentStyle = (new StyleBuilder())->setFontSize(99)->build(); - $mergedStyle = $currentStyle->mergeWith($baseStyle); - - $this->assertEquals(99, $mergedStyle->getFontSize()); - } - - /** - * @return void - */ - public function testMergeWithShouldPreferCurrentStylePropertyIfSetOnCurrentButNotOnBase() - { - $baseStyle = (new StyleBuilder())->build(); - $currentStyle = (new StyleBuilder())->setFontItalic()->setFontStrikethrough()->build(); - $mergedStyle = $currentStyle->mergeWith($baseStyle); - - $this->assertFalse($baseStyle->isFontItalic()); - $this->assertFalse($baseStyle->isFontStrikethrough()); - - $this->assertTrue($mergedStyle->isFontItalic()); - $this->assertTrue($mergedStyle->isFontStrikethrough()); - } - - /** - * @return void - */ - public function testMergeWithShouldPreferBaseStylePropertyIfSetOnBaseButNotOnCurrent() - { - $baseStyle = (new StyleBuilder()) - ->setFontItalic() - ->setFontUnderline() - ->setFontStrikethrough() - ->setShouldWrapText() - ->build(); - $currentStyle = (new StyleBuilder())->build(); - $mergedStyle = $currentStyle->mergeWith($baseStyle); - - $this->assertFalse($currentStyle->isFontUnderline()); - $this->assertTrue($mergedStyle->isFontUnderline()); - - $this->assertFalse($currentStyle->shouldWrapText()); - $this->assertTrue($mergedStyle->shouldWrapText()); - } - - /** - * @return void - */ - public function testMergeWithShouldDoNothingIfStylePropertyNotSetOnBaseNorCurrent() - { - $baseStyle = (new StyleBuilder())->build(); - $currentStyle = (new StyleBuilder())->build(); - $mergedStyle = $currentStyle->mergeWith($baseStyle); - - $this->assertTrue($baseStyle->serialize() === $currentStyle->serialize()); - $this->assertTrue($currentStyle->serialize() === $mergedStyle->serialize()); - } - - /** - * @return void - */ - public function testMergeWithShouldDoNothingIfStylePropertyNotSetOnCurrentAndIsDefaultValueOnBase() - { - $baseStyle = (new StyleBuilder()) - ->setFontName(Style::DEFAULT_FONT_NAME) - ->setFontSize(Style::DEFAULT_FONT_SIZE) - ->build(); - $currentStyle = (new StyleBuilder())->build(); - $mergedStyle = $currentStyle->mergeWith($baseStyle); - - $this->assertTrue($currentStyle->serialize() === $mergedStyle->serialize()); - } - - /** - * @return void - */ - public function testStyleBuilderShouldApplyBorders() - { - $border = (new BorderBuilder()) - ->setBorderBottom() - ->build(); - $style = (new StyleBuilder())->setBorder($border)->build(); - $this->assertTrue($style->shouldApplyBorder()); - } - - /** - * @return void - */ - public function testStyleBuilderShouldMergeBorders() - { - $border = (new BorderBuilder())->setBorderBottom(Color::RED, Border::WIDTH_THIN, Border::STYLE_DASHED)->build(); - - $baseStyle = (new StyleBuilder())->setBorder($border)->build(); - $currentStyle = (new StyleBuilder())->build(); - $mergedStyle = $currentStyle->mergeWith($baseStyle); - - $this->assertEquals(null, $currentStyle->getBorder(), 'Current style has no border'); - $this->assertInstanceOf('Box\Spout\Writer\Style\Border', $baseStyle->getBorder(), 'Base style has a border'); - $this->assertInstanceOf('Box\Spout\Writer\Style\Border', $mergedStyle->getBorder(), 'Merged style has a border'); - } -} diff --git a/tests/Spout/Writer/WriterFactoryTest.php b/tests/Spout/Writer/WriterFactoryTest.php deleted file mode 100644 index 528eff3..0000000 --- a/tests/Spout/Writer/WriterFactoryTest.php +++ /dev/null @@ -1,21 +0,0 @@ -defaultStyle = (new StyleBuilder())->build(); - } - - /** - * @return void - */ - public function testRegisterStyleShouldUpdateId() - { - $style1 = (new StyleBuilder())->setFontBold()->build(); - $style2 = (new StyleBuilder())->setFontUnderline()->build(); - - $this->assertEquals(0, $this->defaultStyle->getId(), 'Default style ID should be 0'); - $this->assertNull($style1->getId()); - $this->assertNull($style2->getId()); - - $styleHelper = new StyleHelper($this->defaultStyle); - $registeredStyle1 = $styleHelper->registerStyle($style1); - $registeredStyle2 = $styleHelper->registerStyle($style2); - - $this->assertEquals(1, $registeredStyle1->getId()); - $this->assertEquals(2, $registeredStyle2->getId()); - } - - /** - * @return void - */ - public function testRegisterStyleShouldReuseAlreadyRegisteredStyles() - { - $style = (new StyleBuilder())->setFontBold()->build(); - - $styleHelper = new StyleHelper($this->defaultStyle); - $registeredStyle1 = $styleHelper->registerStyle($style); - $registeredStyle2 = $styleHelper->registerStyle($style); - - $this->assertEquals(1, $registeredStyle1->getId()); - $this->assertEquals(1, $registeredStyle2->getId()); - } - - /** - * @return void - */ - public function testShouldApplyStyleOnEmptyCell() - { - $styleWithFont = (new StyleBuilder())->setFontBold()->build(); - $styleWithBackground = (new StyleBuilder())->setBackgroundColor(Color::BLUE)->build(); - - $border = (new BorderBuilder())->setBorderBottom(Color::GREEN)->build(); - $styleWithBorder = (new StyleBuilder())->setBorder($border)->build(); - - $styleHelper = new StyleHelper($this->defaultStyle); - $styleHelper->registerStyle($styleWithFont); - $styleHelper->registerStyle($styleWithBackground); - $styleHelper->registerStyle($styleWithBorder); - - $this->assertFalse($styleHelper->shouldApplyStyleOnEmptyCell($styleWithFont->getId())); - $this->assertTrue($styleHelper->shouldApplyStyleOnEmptyCell($styleWithBackground->getId())); - $this->assertTrue($styleHelper->shouldApplyStyleOnEmptyCell($styleWithBorder->getId())); - } - - /** - * @return void - */ - public function testApplyExtraStylesIfNeededShouldApplyWrapTextIfCellContainsNewLine() - { - $style = clone $this->defaultStyle; - $styleHelper = new StyleHelper($this->defaultStyle); - - $this->assertFalse($style->shouldWrapText()); - - $updatedStyle = $styleHelper->applyExtraStylesIfNeeded($style, [12, 'single line', "multi\nlines", null]); - - $this->assertTrue($updatedStyle->shouldWrapText()); - } - - /** - * @return void - */ - public function testApplyExtraStylesIfNeededShouldDoNothingIfWrapTextAlreadyApplied() - { - $style = (new StyleBuilder())->setShouldWrapText()->build(); - $styleHelper = new StyleHelper($this->defaultStyle); - - $this->assertTrue($style->shouldWrapText()); - - $updatedStyle = $styleHelper->applyExtraStylesIfNeeded($style, ["multi\nlines"]); - - $this->assertTrue($updatedStyle->shouldWrapText()); - } -} diff --git a/tests/Spout/Writer/XLSX/SheetTest.php b/tests/Spout/Writer/XLSX/SheetTest.php deleted file mode 100644 index 58c4b05..0000000 --- a/tests/Spout/Writer/XLSX/SheetTest.php +++ /dev/null @@ -1,134 +0,0 @@ -writeDataToMulitpleSheetsAndReturnSheets('test_get_sheet_index.xlsx'); - - $this->assertEquals(2, count($sheets), '2 sheets should have been created'); - $this->assertEquals(0, $sheets[0]->getIndex(), 'The first sheet should be index 0'); - $this->assertEquals(1, $sheets[1]->getIndex(), 'The second sheet should be index 1'); - } - - /** - * @return void - */ - public function testGetSheetName() - { - $sheets = $this->writeDataToMulitpleSheetsAndReturnSheets('test_get_sheet_name.xlsx'); - - $this->assertEquals(2, count($sheets), '2 sheets should have been created'); - $this->assertEquals('Sheet1', $sheets[0]->getName(), 'Invalid name for the first sheet'); - $this->assertEquals('Sheet2', $sheets[1]->getName(), 'Invalid name for the second sheet'); - } - - /** - * @return void - */ - public function testSetSheetNameShouldCreateSheetWithCustomName() - { - $fileName = 'test_set_name_should_create_sheet_with_custom_name.xlsx'; - $customSheetName = 'CustomName'; - $this->writeDataAndReturnSheetWithCustomName($fileName, $customSheetName); - - $this->assertSheetNameEquals($customSheetName, $fileName, "The sheet name should have been changed to '$customSheetName'"); - } - - /** - * @expectedException \Box\Spout\Writer\Exception\InvalidSheetNameException - * @return void - */ - public function testSetSheetNameShouldThrowWhenNameIsAlreadyUsed() - { - $fileName = 'test_set_name_with_non_unique_name.xlsx'; - $this->createGeneratedFolderIfNeeded($fileName); - $resourcePath = $this->getGeneratedResourcePath($fileName); - - $writer = WriterFactory::create(Type::XLSX); - $writer->openToFile($resourcePath); - - $customSheetName = 'Sheet name'; - - $sheet = $writer->getCurrentSheet(); - $sheet->setName($customSheetName); - - $writer->addNewSheetAndMakeItCurrent(); - $sheet = $writer->getCurrentSheet(); - $sheet->setName($customSheetName); - } - - /** - * @param string $fileName - * @param string $sheetName - * @return Sheet - */ - private function writeDataAndReturnSheetWithCustomName($fileName, $sheetName) - { - $this->createGeneratedFolderIfNeeded($fileName); - $resourcePath = $this->getGeneratedResourcePath($fileName); - - $writer = WriterFactory::create(Type::XLSX); - $writer->openToFile($resourcePath); - - $sheet = $writer->getCurrentSheet(); - $sheet->setName($sheetName); - - $writer->addRow(['xlsx--11', 'xlsx--12']); - $writer->close(); - } - - /** - * @param string $fileName - * @return Sheet[] - */ - private function writeDataToMulitpleSheetsAndReturnSheets($fileName) - { - $this->createGeneratedFolderIfNeeded($fileName); - $resourcePath = $this->getGeneratedResourcePath($fileName); - - /** @var \Box\Spout\Writer\XLSX\Writer $writer */ - $writer = WriterFactory::create(Type::XLSX); - $writer->openToFile($resourcePath); - - $writer->addRow(['xlsx--sheet1--11', 'xlsx--sheet1--12']); - $writer->addNewSheetAndMakeItCurrent(); - $writer->addRow(['xlsx--sheet2--11', 'xlsx--sheet2--12', 'xlsx--sheet2--13']); - - $writer->close(); - - return $writer->getSheets(); - } - - /** - * @param string $expectedName - * @param string $fileName - * @param string $message - * @return void - */ - private function assertSheetNameEquals($expectedName, $fileName, $message = '') - { - $resourcePath = $this->getGeneratedResourcePath($fileName); - $pathToWorkbookFile = $resourcePath . '#xl/workbook.xml'; - $xmlContents = file_get_contents('zip://' . $pathToWorkbookFile); - - $this->assertContains("createUnwritableFolderIfNeeded(); - $filePath = $this->getGeneratedUnwritableResourcePath($fileName); - - $writer = WriterFactory::create(Type::XLSX); - @$writer->openToFile($filePath); - } - - /** - * @expectedException \Box\Spout\Writer\Exception\WriterNotOpenedException - */ - public function testAddRowShouldThrowExceptionIfCallAddRowBeforeOpeningWriter() - { - $writer = WriterFactory::create(Type::XLSX); - $writer->addRow(['xlsx--11', 'xlsx--12']); - } - - /** - * @expectedException \Box\Spout\Writer\Exception\WriterNotOpenedException - */ - public function testAddRowShouldThrowExceptionIfCalledBeforeOpeningWriter() - { - $writer = WriterFactory::create(Type::XLSX); - $writer->addRows([['xlsx--11', 'xlsx--12']]); - } - - /** - * @expectedException \Box\Spout\Writer\Exception\WriterAlreadyOpenedException - */ - public function testSetTempFolderShouldThrowExceptionIfCalledAfterOpeningWriter() - { - $fileName = 'file_that_wont_be_written.xlsx'; - $filePath = $this->getGeneratedResourcePath($fileName); - - /** @var \Box\Spout\Writer\XLSX\Writer $writer */ - $writer = WriterFactory::create(Type::XLSX); - $writer->openToFile($filePath); - - $writer->setTempFolder(''); - } - - /** - * @expectedException \Box\Spout\Writer\Exception\WriterAlreadyOpenedException - */ - public function testSetShouldUseInlineStringsShouldThrowExceptionIfCalledAfterOpeningWriter() - { - $fileName = 'file_that_wont_be_written.xlsx'; - $filePath = $this->getGeneratedResourcePath($fileName); - - /** @var \Box\Spout\Writer\XLSX\Writer $writer */ - $writer = WriterFactory::create(Type::XLSX); - $writer->openToFile($filePath); - - $writer->setShouldUseInlineStrings(true); - } - - /** - * @expectedException \Box\Spout\Writer\Exception\WriterAlreadyOpenedException - */ - public function testsetShouldCreateNewSheetsAutomaticallyShouldThrowExceptionIfCalledAfterOpeningWriter() - { - $fileName = 'file_that_wont_be_written.xlsx'; - $filePath = $this->getGeneratedResourcePath($fileName); - - /** @var \Box\Spout\Writer\XLSX\Writer $writer */ - $writer = WriterFactory::create(Type::XLSX); - $writer->openToFile($filePath); - - $writer->setShouldCreateNewSheetsAutomatically(true); - } - - /** - * @expectedException \Box\Spout\Common\Exception\InvalidArgumentException - */ - public function testAddRowShouldThrowExceptionIfUnsupportedDataTypePassedIn() - { - $fileName = 'test_add_row_should_throw_exception_if_unsupported_data_type_passed_in.xlsx'; - $dataRows = [ - [str_repeat('a', Worksheet::MAX_CHARACTERS_PER_CELL + 1)], - ]; - - $this->writeToXLSXFile($dataRows, $fileName); - } - - /** - * @expectedException \Box\Spout\Common\Exception\InvalidArgumentException - */ - public function testAddRowShouldThrowExceptionIfWritingStringExceedingMaxNumberOfCharactersAllowedPerCell() - { - $fileName = 'test_add_row_should_throw_exception_if_string_exceeds_max_num_chars_allowed_per_cell.xlsx'; - $dataRows = [ - [new \stdClass()], - ]; - - $this->writeToXLSXFile($dataRows, $fileName); - } - - /** - * @return void - */ - public function testAddRowShouldCleanupAllFilesIfExceptionIsThrown() - { - $fileName = 'test_add_row_should_cleanup_all_files_if_exception_thrown.xlsx'; - $dataRows = [ - ['wrong'], - [new \stdClass()], - ]; - - $this->createGeneratedFolderIfNeeded($fileName); - $resourcePath = $this->getGeneratedResourcePath($fileName); - - $this->recreateTempFolder(); - $tempFolderPath = $this->getTempFolderPath(); - - /** @var \Box\Spout\Writer\XLSX\Writer $writer */ - $writer = WriterFactory::create(Type::XLSX); - $writer->setTempFolder($tempFolderPath); - $writer->openToFile($resourcePath); - - try { - $writer->addRows($dataRows); - $this->fail('Exception should have been thrown'); - } catch (SpoutException $e) { - $this->assertFalse(file_exists($fileName), 'Output file should have been deleted'); - - $numFiles = iterator_count(new \FilesystemIterator($tempFolderPath, \FilesystemIterator::SKIP_DOTS)); - $this->assertEquals(0, $numFiles, 'All temp files should have been deleted'); - } - } - - /** - * @return void - */ - public function testAddNewSheetAndMakeItCurrent() - { - $fileName = 'test_add_new_sheet_and_make_it_current.xlsx'; - $this->createGeneratedFolderIfNeeded($fileName); - $resourcePath = $this->getGeneratedResourcePath($fileName); - - $writer = WriterFactory::create(Type::XLSX); - $writer->openToFile($resourcePath); - $writer->addNewSheetAndMakeItCurrent(); - $writer->close(); - - $sheets = $writer->getSheets(); - $this->assertEquals(2, count($sheets), 'There should be 2 sheets'); - $this->assertEquals($sheets[1], $writer->getCurrentSheet(), 'The current sheet should be the second one.'); - } - - /** - * @return void - */ - public function testSetCurrentSheet() - { - $fileName = 'test_set_current_sheet.xlsx'; - $this->createGeneratedFolderIfNeeded($fileName); - $resourcePath = $this->getGeneratedResourcePath($fileName); - - $writer = WriterFactory::create(Type::XLSX); - $writer->openToFile($resourcePath); - - $writer->addNewSheetAndMakeItCurrent(); - $writer->addNewSheetAndMakeItCurrent(); - - $firstSheet = $writer->getSheets()[0]; - $writer->setCurrentSheet($firstSheet); - - $writer->close(); - - $this->assertEquals($firstSheet, $writer->getCurrentSheet(), 'The current sheet should be the first one.'); - } - - /** - * @return void - */ - public function testCloseShouldNoopWhenWriterIsNotOpened() - { - $fileName = 'test_double_close_calls.xlsx'; - $this->createGeneratedFolderIfNeeded($fileName); - $resourcePath = $this->getGeneratedResourcePath($fileName); - - $writer = WriterFactory::create(Type::XLSX); - $writer->close(); // This call should not cause any error - - $writer->openToFile($resourcePath); - $writer->close(); - $writer->close(); // This call should not cause any error - } - - /** - * @return void - */ - public function testAddRowShouldWriteGivenDataToSheetUsingInlineStrings() - { - $fileName = 'test_add_row_should_write_given_data_to_sheet_using_inline_strings.xlsx'; - $dataRows = [ - ['xlsx--11', 'xlsx--12'], - ['xlsx--21', 'xlsx--22', 'xlsx--23'], - ]; - - $this->writeToXLSXFile($dataRows, $fileName, $shouldUseInlineStrings = true); - - foreach ($dataRows as $dataRow) { - foreach ($dataRow as $cellValue) { - $this->assertInlineDataWasWrittenToSheet($fileName, 1, $cellValue); - } - } - } - - /** - * @return void - */ - public function testAddRowShouldWriteGivenDataToTwoSheetsUsingInlineStrings() - { - $fileName = 'test_add_row_should_write_given_data_to_two_sheets_using_inline_strings.xlsx'; - $dataRows = [ - ['xlsx--11', 'xlsx--12'], - ['xlsx--21', 'xlsx--22', 'xlsx--23'], - ]; - - $numSheets = 2; - $this->writeToMultipleSheetsInXLSXFile($dataRows, $numSheets, $fileName, $shouldUseInlineStrings = true); - - for ($i = 1; $i <= $numSheets; $i++) { - foreach ($dataRows as $dataRow) { - foreach ($dataRow as $cellValue) { - $this->assertInlineDataWasWrittenToSheet($fileName, $numSheets, $cellValue); - } - } - } - } - - /** - * @return void - */ - public function testAddRowShouldWriteGivenDataToSheetUsingSharedStrings() - { - $fileName = 'test_add_row_should_write_given_data_to_sheet_using_shared_strings.xlsx'; - $dataRows = [ - ['xlsx--11', 'xlsx--12'], - ['xlsx--21', 'xlsx--22', 'xlsx--23'], - ]; - - $this->writeToXLSXFile($dataRows, $fileName, $shouldUseInlineStrings = false); - - foreach ($dataRows as $dataRow) { - foreach ($dataRow as $cellValue) { - $this->assertSharedStringWasWritten($fileName, $cellValue); - } - } - } - - /** - * @return void - */ - public function testAddRowShouldWriteGivenDataToTwoSheetsUsingSharedStrings() - { - $fileName = 'test_add_row_should_write_given_data_to_two_sheets_using_shared_strings.xlsx'; - $dataRows = [ - ['xlsx--11', 'xlsx--12'], - ['xlsx--21', 'xlsx--22', 'xlsx--23'], - ]; - - $numSheets = 2; - $this->writeToMultipleSheetsInXLSXFile($dataRows, $numSheets, $fileName, $shouldUseInlineStrings = false); - - for ($i = 1; $i <= $numSheets; $i++) { - foreach ($dataRows as $dataRow) { - foreach ($dataRow as $cellValue) { - $this->assertSharedStringWasWritten($fileName, $cellValue); - } - } - } - } - - /** - * @return void - */ - public function testAddRowShouldSupportAssociativeArrays() - { - $fileName = 'test_add_row_should_support_associative_arrays.xlsx'; - $dataRows = [ - ['foo' => 'xlsx--11', 'bar' => 'xlsx--12'], - ]; - - $this->writeToXLSXFile($dataRows, $fileName); - - foreach ($dataRows as $dataRow) { - foreach ($dataRow as $cellValue) { - $this->assertInlineDataWasWrittenToSheet($fileName, 1, $cellValue); - } - } - } - - /** - * @return void - */ - public function testAddRowShouldNotWriteEmptyRows() - { - $fileName = 'test_add_row_should_not_write_empty_rows.xlsx'; - $dataRows = [ - [''], - ['xlsx--21', 'xlsx--22'], - ['key' => ''], - [''], - ['xlsx--51', 'xlsx--52'], - ]; - - $this->writeToXLSXFile($dataRows, $fileName); - - $this->assertInlineDataWasWrittenToSheet($fileName, 1, 'row r="2"'); - $this->assertInlineDataWasWrittenToSheet($fileName, 1, 'row r="5"'); - $this->assertInlineDataWasNotWrittenToSheet($fileName, 1, 'row r="1"'); - $this->assertInlineDataWasNotWrittenToSheet($fileName, 1, 'row r="3"'); - $this->assertInlineDataWasNotWrittenToSheet($fileName, 1, 'row r="4"'); - } - - /** - * @return void - */ - public function testAddRowShouldSupportMultipleTypesOfData() - { - $fileName = 'test_add_row_should_support_multiple_types_of_data.xlsx'; - $dataRows = [ - ['xlsx--11', true, '', 0, 10.2, null], - ]; - - $this->writeToXLSXFile($dataRows, $fileName, $shouldUseInlineStrings = false); - - $this->assertSharedStringWasWritten($fileName, 'xlsx--11'); - $this->assertInlineDataWasWrittenToSheet($fileName, 1, 1); // true is converted to 1 - $this->assertInlineDataWasWrittenToSheet($fileName, 1, 0); - $this->assertInlineDataWasWrittenToSheet($fileName, 1, 10.2); - } - - /** - * @return void - */ - public function testAddRowShouldWriteGivenDataToTheCorrectSheet() - { - $fileName = 'test_add_row_should_write_given_data_to_the_correct_sheet.xlsx'; - $dataRowsSheet1 = [ - ['xlsx--sheet1--11', 'xlsx--sheet1--12'], - ['xlsx--sheet1--21', 'xlsx--sheet1--22', 'xlsx--sheet1--23'], - ]; - $dataRowsSheet2 = [ - ['xlsx--sheet2--11', 'xlsx--sheet2--12'], - ['xlsx--sheet2--21', 'xlsx--sheet2--22', 'xlsx--sheet2--23'], - ]; - $dataRowsSheet1Again = [ - ['xlsx--sheet1--31', 'xlsx--sheet1--32'], - ['xlsx--sheet1--41', 'xlsx--sheet1--42', 'xlsx--sheet1--43'], - ]; - - $this->createGeneratedFolderIfNeeded($fileName); - $resourcePath = $this->getGeneratedResourcePath($fileName); - - /** @var \Box\Spout\Writer\XLSX\Writer $writer */ - $writer = WriterFactory::create(Type::XLSX); - $writer->setShouldUseInlineStrings(true); - - $writer->openToFile($resourcePath); - - $writer->addRows($dataRowsSheet1); - - $writer->addNewSheetAndMakeItCurrent(); - $writer->addRows($dataRowsSheet2); - - $firstSheet = $writer->getSheets()[0]; - $writer->setCurrentSheet($firstSheet); - - $writer->addRows($dataRowsSheet1Again); - - $writer->close(); - - foreach ($dataRowsSheet1 as $dataRow) { - foreach ($dataRow as $cellValue) { - $this->assertInlineDataWasWrittenToSheet($fileName, 1, $cellValue, 'Data should have been written in Sheet 1'); - } - } - foreach ($dataRowsSheet2 as $dataRow) { - foreach ($dataRow as $cellValue) { - $this->assertInlineDataWasWrittenToSheet($fileName, 2, $cellValue, 'Data should have been written in Sheet 2'); - } - } - foreach ($dataRowsSheet1Again as $dataRow) { - foreach ($dataRow as $cellValue) { - $this->assertInlineDataWasWrittenToSheet($fileName, 1, $cellValue, 'Data should have been written in Sheet 1'); - } - } - } - - /** - * @return void - */ - public function testAddRowShouldAutomaticallyCreateNewSheetsIfMaxRowsReachedAndOptionTurnedOn() - { - $fileName = 'test_add_row_should_automatically_create_new_sheets_if_max_rows_reached_and_option_turned_on.xlsx'; - $dataRows = [ - ['xlsx--sheet1--11', 'xlsx--sheet1--12'], - ['xlsx--sheet1--21', 'xlsx--sheet1--22', 'xlsx--sheet1--23'], - ['xlsx--sheet2--11', 'xlsx--sheet2--12'], // this should be written in a new sheet - ]; - - // set the maxRowsPerSheet limit to 2 - \ReflectionHelper::setStaticValue('\Box\Spout\Writer\XLSX\Internal\Workbook', 'maxRowsPerWorksheet', 2); - - $writer = $this->writeToXLSXFile($dataRows, $fileName, true, $shouldCreateSheetsAutomatically = true); - $this->assertEquals(2, count($writer->getSheets()), '2 sheets should have been created.'); - - $this->assertInlineDataWasNotWrittenToSheet($fileName, 1, 'xlsx--sheet2--11'); - $this->assertInlineDataWasWrittenToSheet($fileName, 2, 'xlsx--sheet2--11'); - - \ReflectionHelper::reset(); - } - - /** - * @return void - */ - public function testAddRowShouldNotCreateNewSheetsIfMaxRowsReachedAndOptionTurnedOff() - { - $fileName = 'test_add_row_should_not_create_new_sheets_if_max_rows_reached_and_option_turned_off.xlsx'; - $dataRows = [ - ['xlsx--sheet1--11', 'xlsx--sheet1--12'], - ['xlsx--sheet1--21', 'xlsx--sheet1--22', 'xlsx--sheet1--23'], - ['xlsx--sheet1--31', 'xlsx--sheet1--32'], // this should NOT be written in a new sheet - ]; - - // set the maxRowsPerSheet limit to 2 - \ReflectionHelper::setStaticValue('\Box\Spout\Writer\XLSX\Internal\Workbook', 'maxRowsPerWorksheet', 2); - - $writer = $this->writeToXLSXFile($dataRows, $fileName, true, $shouldCreateSheetsAutomatically = false); - $this->assertEquals(1, count($writer->getSheets()), 'Only 1 sheet should have been created.'); - - $this->assertInlineDataWasNotWrittenToSheet($fileName, 1, 'xlsx--sheet1--31'); - - \ReflectionHelper::reset(); - } - - /** - * @return void - */ - public function testAddRowShouldEscapeHtmlSpecialCharacters() - { - $fileName = 'test_add_row_should_escape_html_special_characters.xlsx'; - $dataRows = [ - ['I\'m in "great" mood', 'This be escaped & tested'], - ]; - - $this->writeToXLSXFile($dataRows, $fileName); - - $this->assertInlineDataWasWrittenToSheet($fileName, 1, 'I\'m in "great" mood', 'Quotes should not be escaped'); - $this->assertInlineDataWasWrittenToSheet($fileName, 1, 'This <must> be escaped & tested', '<, > and & should be escaped'); - } - - /** - * @return void - */ - public function testAddRowShouldEscapeControlCharacters() - { - $fileName = 'test_add_row_should_escape_control_characters.xlsx'; - $dataRows = [ - ['control '.chr(21).' character'], - ]; - - $this->writeToXLSXFile($dataRows, $fileName); - - $this->assertInlineDataWasWrittenToSheet($fileName, 1, 'control _x0015_ character'); - } - - /** - * @return void - */ - public function testGeneratedFileShouldHaveTheCorrectMimeType() - { - $fileName = 'test_mime_type.xlsx'; - $resourcePath = $this->getGeneratedResourcePath($fileName); - $dataRows = [['foo']]; - - $this->writeToXLSXFile($dataRows, $fileName); - - $finfo = new \finfo(FILEINFO_MIME_TYPE); - $this->assertEquals('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', $finfo->file($resourcePath)); - } - - /** - * @param array $allRows - * @param string $fileName - * @param bool $shouldUseInlineStrings - * @param bool $shouldCreateSheetsAutomatically - * @return Writer - */ - private function writeToXLSXFile($allRows, $fileName, $shouldUseInlineStrings = true, $shouldCreateSheetsAutomatically = true) - { - $this->createGeneratedFolderIfNeeded($fileName); - $resourcePath = $this->getGeneratedResourcePath($fileName); - - /** @var \Box\Spout\Writer\XLSX\Writer $writer */ - $writer = WriterFactory::create(Type::XLSX); - $writer->setShouldUseInlineStrings($shouldUseInlineStrings); - $writer->setShouldCreateNewSheetsAutomatically($shouldCreateSheetsAutomatically); - - $writer->openToFile($resourcePath); - $writer->addRows($allRows); - $writer->close(); - - return $writer; - } - - /** - * @param array $allRows - * @param int $numSheets - * @param string $fileName - * @param bool $shouldUseInlineStrings - * @param bool $shouldCreateSheetsAutomatically - * @return Writer - */ - private function writeToMultipleSheetsInXLSXFile($allRows, $numSheets, $fileName, $shouldUseInlineStrings = true, $shouldCreateSheetsAutomatically = true) - { - $this->createGeneratedFolderIfNeeded($fileName); - $resourcePath = $this->getGeneratedResourcePath($fileName); - - /** @var \Box\Spout\Writer\XLSX\Writer $writer */ - $writer = WriterFactory::create(Type::XLSX); - $writer->setShouldUseInlineStrings($shouldUseInlineStrings); - $writer->setShouldCreateNewSheetsAutomatically($shouldCreateSheetsAutomatically); - - $writer->openToFile($resourcePath); - $writer->addRows($allRows); - - for ($i = 1; $i < $numSheets; $i++) { - $writer->addNewSheetAndMakeItCurrent(); - $writer->addRows($allRows); - } - - $writer->close(); - - return $writer; - } - - /** - * @param string $fileName - * @param int $sheetIndex - * @param mixed $inlineData - * @param string $message - * @return void - */ - private function assertInlineDataWasWrittenToSheet($fileName, $sheetIndex, $inlineData, $message = '') - { - $resourcePath = $this->getGeneratedResourcePath($fileName); - $pathToSheetFile = $resourcePath . '#xl/worksheets/sheet' . $sheetIndex . '.xml'; - $xmlContents = file_get_contents('zip://' . $pathToSheetFile); - - $this->assertContains((string)$inlineData, $xmlContents, $message); - } - - /** - * @param string $fileName - * @param int $sheetIndex - * @param mixed $inlineData - * @param string $message - * @return void - */ - private function assertInlineDataWasNotWrittenToSheet($fileName, $sheetIndex, $inlineData, $message = '') - { - $resourcePath = $this->getGeneratedResourcePath($fileName); - $pathToSheetFile = $resourcePath . '#xl/worksheets/sheet' . $sheetIndex . '.xml'; - $xmlContents = file_get_contents('zip://' . $pathToSheetFile); - - $this->assertNotContains((string)$inlineData, $xmlContents, $message); - } - - /** - * @param string $fileName - * @param string $sharedString - * @param string $message - * @return void - */ - private function assertSharedStringWasWritten($fileName, $sharedString, $message = '') - { - $resourcePath = $this->getGeneratedResourcePath($fileName); - $pathToSharedStringsFile = $resourcePath . '#xl/sharedStrings.xml'; - $xmlContents = file_get_contents('zip://' . $pathToSharedStringsFile); - - $this->assertContains($sharedString, $xmlContents, $message); - } -} diff --git a/tests/Spout/Writer/XLSX/WriterWithStyleTest.php b/tests/Spout/Writer/XLSX/WriterWithStyleTest.php deleted file mode 100644 index e4d1749..0000000 --- a/tests/Spout/Writer/XLSX/WriterWithStyleTest.php +++ /dev/null @@ -1,668 +0,0 @@ -defaultStyle = (new StyleBuilder())->build(); - } - - /** - * @expectedException \Box\Spout\Writer\Exception\WriterNotOpenedException - */ - public function testAddRowWithStyleShouldThrowExceptionIfCallAddRowBeforeOpeningWriter() - { - $writer = WriterFactory::create(Type::XLSX); - $writer->addRowWithStyle(['xlsx--11', 'xlsx--12'], $this->defaultStyle); - } - - /** - * @expectedException \Box\Spout\Writer\Exception\WriterNotOpenedException - */ - public function testAddRowWithStyleShouldThrowExceptionIfCalledBeforeOpeningWriter() - { - $writer = WriterFactory::create(Type::XLSX); - $writer->addRowWithStyle(['xlsx--11', 'xlsx--12'], $this->defaultStyle); - } - - /** - * @return array - */ - public function dataProviderForInvalidStyle() - { - return [ - ['style'], - [new \stdClass()], - [null], - ]; - } - - /** - * @dataProvider dataProviderForInvalidStyle - * @expectedException \Box\Spout\Common\Exception\InvalidArgumentException - * - * @param \Box\Spout\Writer\Style\Style $style - */ - public function testAddRowWithStyleShouldThrowExceptionIfInvalidStyleGiven($style) - { - $fileName = 'test_add_row_with_style_should_throw_exception.xlsx'; - $this->createGeneratedFolderIfNeeded($fileName); - $resourcePath = $this->getGeneratedResourcePath($fileName); - - $writer = WriterFactory::create(Type::XLSX); - $writer->openToFile($resourcePath); - $writer->addRowWithStyle(['xlsx--11', 'xlsx--12'], $style); - } - - /** - * @dataProvider dataProviderForInvalidStyle - * @expectedException \Box\Spout\Common\Exception\InvalidArgumentException - * - * @param \Box\Spout\Writer\Style\Style $style - */ - public function testAddRowsWithStyleShouldThrowExceptionIfInvalidStyleGiven($style) - { - $fileName = 'test_add_row_with_style_should_throw_exception.xlsx'; - $this->createGeneratedFolderIfNeeded($fileName); - $resourcePath = $this->getGeneratedResourcePath($fileName); - - $writer = WriterFactory::create(Type::XLSX); - $writer->openToFile($resourcePath); - $writer->addRowsWithStyle([['xlsx--11', 'xlsx--12']], $style); - } - - /** - * @return void - */ - public function testAddRowWithStyleShouldListAllUsedFontsInCreatedStylesXmlFile() - { - $fileName = 'test_add_row_with_style_should_list_all_used_fonts.xlsx'; - $dataRows = [ - ['xlsx--11', 'xlsx--12'], - ['xlsx--21', 'xlsx--22'], - ]; - - $style = (new StyleBuilder()) - ->setFontBold() - ->setFontItalic() - ->setFontUnderline() - ->setFontStrikethrough() - ->build(); - $style2 = (new StyleBuilder()) - ->setFontSize(15) - ->setFontColor(Color::RED) - ->setFontName('Cambria') - ->build(); - - $this->writeToXLSXFileWithMultipleStyles($dataRows, $fileName, [$style, $style2]); - - $fontsDomElement = $this->getXmlSectionFromStylesXmlFile($fileName, 'fonts'); - $this->assertEquals(3, $fontsDomElement->getAttribute('count'), 'There should be 3 fonts, including the default one.'); - - $fontElements = $fontsDomElement->getElementsByTagName('font'); - $this->assertEquals(3, $fontElements->length, 'There should be 3 associated "font" elements, including the default one.'); - // First font should be the default one - $defaultFontElement = $fontElements->item(0); - $this->assertChildrenNumEquals(3, $defaultFontElement, 'The default font should only have 3 properties.'); - $this->assertFirstChildHasAttributeEquals((string) Writer::DEFAULT_FONT_SIZE, $defaultFontElement, 'sz', 'val'); - $this->assertFirstChildHasAttributeEquals(Color::toARGB(Style::DEFAULT_FONT_COLOR), $defaultFontElement, 'color', 'rgb'); - $this->assertFirstChildHasAttributeEquals(Writer::DEFAULT_FONT_NAME, $defaultFontElement, 'name', 'val'); - - // Second font should contain data from the first created style - $secondFontElement = $fontElements->item(1); - $this->assertChildrenNumEquals(7, $secondFontElement, 'The font should only have 7 properties (4 custom styles + 3 default styles).'); - $this->assertChildExists($secondFontElement, 'b'); - $this->assertChildExists($secondFontElement, 'i'); - $this->assertChildExists($secondFontElement, 'u'); - $this->assertChildExists($secondFontElement, 'strike'); - $this->assertFirstChildHasAttributeEquals((string) Writer::DEFAULT_FONT_SIZE, $secondFontElement, 'sz', 'val'); - $this->assertFirstChildHasAttributeEquals(Color::toARGB(Style::DEFAULT_FONT_COLOR), $secondFontElement, 'color', 'rgb'); - $this->assertFirstChildHasAttributeEquals(Writer::DEFAULT_FONT_NAME, $secondFontElement, 'name', 'val'); - - // Third font should contain data from the second created style - $thirdFontElement = $fontElements->item(2); - $this->assertChildrenNumEquals(3, $thirdFontElement, 'The font should only have 3 properties.'); - $this->assertFirstChildHasAttributeEquals('15', $thirdFontElement, 'sz', 'val'); - $this->assertFirstChildHasAttributeEquals(Color::toARGB(Color::RED), $thirdFontElement, 'color', 'rgb'); - $this->assertFirstChildHasAttributeEquals('Cambria', $thirdFontElement, 'name', 'val'); - } - - /** - * @return void - */ - public function testAddRowWithStyleShouldApplyStyleToCells() - { - $fileName = 'test_add_row_with_style_should_apply_style_to_cells.xlsx'; - $dataRows = [ - ['xlsx--11'], - ['xlsx--21'], - ['xlsx--31'], - ]; - $style = (new StyleBuilder())->setFontBold()->build(); - $style2 = (new StyleBuilder())->setFontSize(15)->build(); - - $this->writeToXLSXFileWithMultipleStyles($dataRows, $fileName, [$style, $style2, null]); - - $cellDomElements = $this->getCellElementsFromSheetXmlFile($fileName); - $this->assertEquals(3, count($cellDomElements), 'There should be 3 cells.'); - - $this->assertEquals('1', $cellDomElements[0]->getAttribute('s')); - $this->assertEquals('2', $cellDomElements[1]->getAttribute('s')); - $this->assertEquals('0', $cellDomElements[2]->getAttribute('s')); - } - - /** - * @return void - */ - public function testAddRowWithStyleShouldApplyStyleToEmptyCellsIfNeeded() - { - $fileName = 'test_add_row_with_style_should_apply_style_to_empty_cells_if_needed.xlsx'; - $dataRows = [ - ['xlsx--11', '', 'xlsx--13'], - ['xlsx--21', '', 'xlsx--23'], - ['xlsx--31', '', 'xlsx--33'], - ['xlsx--41', '', 'xlsx--43'], - ]; - - $styleWithFont = (new StyleBuilder())->setFontBold()->build(); - $styleWithBackground = (new StyleBuilder())->setBackgroundColor(Color::BLUE)->build(); - - $border = (new BorderBuilder())->setBorderBottom(Color::GREEN)->build(); - $styleWithBorder = (new StyleBuilder())->setBorder($border)->build(); - - $this->writeToXLSXFileWithMultipleStyles($dataRows, $fileName, [null, $styleWithFont, $styleWithBackground, $styleWithBorder]); - - $cellDomElements = $this->getCellElementsFromSheetXmlFile($fileName); - - // The first and second rows should not have a reference to the empty cell - // The other rows should have the reference because style should be applied to them - // So that's: 2 + 2 + 3 + 3 = 10 cells - $this->assertEquals(10, count($cellDomElements)); - - // First row has 2 styled cells - $this->assertEquals('0', $cellDomElements[0]->getAttribute('s')); - $this->assertEquals('0', $cellDomElements[1]->getAttribute('s')); - - // Second row has 2 styled cells - $this->assertEquals('1', $cellDomElements[2]->getAttribute('s')); - $this->assertEquals('1', $cellDomElements[3]->getAttribute('s')); - - // Third row has 3 styled cells - $this->assertEquals('2', $cellDomElements[4]->getAttribute('s')); - $this->assertEquals('2', $cellDomElements[5]->getAttribute('s')); - $this->assertEquals('2', $cellDomElements[6]->getAttribute('s')); - - // Third row has 3 styled cells - $this->assertEquals('3', $cellDomElements[7]->getAttribute('s')); - $this->assertEquals('3', $cellDomElements[8]->getAttribute('s')); - $this->assertEquals('3', $cellDomElements[9]->getAttribute('s')); - } - - /** - * @return void - */ - public function testAddRowWithStyleShouldReuseDuplicateStyles() - { - $fileName = 'test_add_row_with_style_should_reuse_duplicate_styles.xlsx'; - $dataRows = [ - ['xlsx--11'], - ['xlsx--21'], - ]; - $style = (new StyleBuilder())->setFontBold()->build(); - - $this->writeToXLSXFile($dataRows, $fileName, $style); - - $cellDomElements = $this->getCellElementsFromSheetXmlFile($fileName); - $this->assertEquals('1', $cellDomElements[0]->getAttribute('s')); - $this->assertEquals('1', $cellDomElements[1]->getAttribute('s')); - } - - /** - * @return void - */ - public function testAddRowWithStyleShouldAddWrapTextAlignmentInfoInStylesXmlFileIfSpecified() - { - $fileName = 'test_add_row_with_style_should_add_wrap_text_alignment.xlsx'; - $dataRows = [ - ['xlsx--11', 'xlsx--12'], - ]; - $style = (new StyleBuilder())->setShouldWrapText()->build(); - - $this->writeToXLSXFile($dataRows, $fileName, $style); - - $cellXfsDomElement = $this->getXmlSectionFromStylesXmlFile($fileName, 'cellXfs'); - $xfElement = $cellXfsDomElement->getElementsByTagName('xf')->item(1); - $this->assertEquals(1, $xfElement->getAttribute('applyAlignment')); - $this->assertFirstChildHasAttributeEquals('1', $xfElement, 'alignment', 'wrapText'); - } - - /** - * @return void - */ - public function testAddRowWithStyleShouldApplyWrapTextIfCellContainsNewLine() - { - $fileName = 'test_add_row_with_style_should_apply_wrap_text_if_new_lines.xlsx'; - $dataRows = [ - ["xlsx--11\nxlsx--11"], - ['xlsx--21'], - ]; - - $this->writeToXLSXFile($dataRows, $fileName, $this->defaultStyle); - - $cellXfsDomElement = $this->getXmlSectionFromStylesXmlFile($fileName, 'cellXfs'); - $xfElement = $cellXfsDomElement->getElementsByTagName('xf')->item(1); - $this->assertEquals(1, $xfElement->getAttribute('applyAlignment')); - $this->assertFirstChildHasAttributeEquals('1', $xfElement, 'alignment', 'wrapText'); - } - - /** - * @return void - */ - public function testAddBackgroundColor() - { - $fileName = 'test_add_background_color.xlsx'; - $dataRows = [ - ["BgColor"], - ]; - $style = (new StyleBuilder())->setBackgroundColor(Color::WHITE)->build(); - $this->writeToXLSXFile($dataRows, $fileName, $style); - $fillsDomElement = $this->getXmlSectionFromStylesXmlFile($fileName, 'fills'); - $this->assertEquals(3, $fillsDomElement->getAttribute('count'), 'There should be 3 fills, including the 2 default ones'); - - $fillsElements = $fillsDomElement->getElementsByTagName('fill'); - - $thirdFillElement = $fillsElements->item(2); // Zero based - $fgColor = $thirdFillElement->getElementsByTagName('fgColor')->item(0)->getAttribute('rgb'); - - $this->assertEquals(Color::WHITE, $fgColor, 'The foreground color should equal white'); - - $styleXfsElements = $this->getXmlSectionFromStylesXmlFile($fileName, 'cellXfs'); - $this->assertEquals(2, $styleXfsElements->getAttribute('count'), '2 cell xfs present - a default one and a custom one'); - - $customFillId = $styleXfsElements->lastChild->getAttribute('fillId'); - $this->assertEquals(2, (int)$customFillId, 'The custom fill id should have the index 2'); - } - - /** - * @return void - */ - public function testReuseBackgroundColorSharedDefinition() - { - $fileName = 'test_add_background_color_shared_definition.xlsx'; - $dataRows = [ - ["row-bold-background-red"], - ["row-background-red"], - ]; - - $styles = [ - (new StyleBuilder())->setBackgroundColor(Color::RED)->setFontBold()->build(), - (new StyleBuilder())->setBackgroundColor(Color::RED)->build() - ]; - - $this->writeToXLSXFileWithMultipleStyles($dataRows, $fileName, $styles); - - $fillsDomElement = $this->getXmlSectionFromStylesXmlFile($fileName, 'fills'); - $this->assertEquals( - 3, - $fillsDomElement->getAttribute('count'), - 'There should be 3 fills, including the 2 default ones' - ); - - $styleXfsElements = $this->getXmlSectionFromStylesXmlFile($fileName, 'cellXfs'); - $this->assertEquals( - 3, - $styleXfsElements->getAttribute('count'), - '3 cell xfs present - a default one and two custom ones' - ); - - $firstCustomId = $styleXfsElements->childNodes->item(1)->getAttribute('fillId'); - $this->assertEquals(2, (int)$firstCustomId, 'The first custom fill id should have the index 2'); - - $secondCustomId = $styleXfsElements->childNodes->item(2)->getAttribute('fillId'); - $this->assertEquals(2, (int)$secondCustomId, 'The second custom fill id should have the index 2'); - } - - /** - * @return void - */ - public function testBorders() - { - $fileName = 'test_borders.xlsx'; - - $dataRows = [ - ['row-with-border-bottom-green-thick-solid'], - ['row-without-border'], - ['row-with-border-top-red-thin-dashed'], - ]; - - $borderBottomGreenThickSolid = (new BorderBuilder()) - ->setBorderBottom(Color::GREEN, Border::WIDTH_THICK, Border::STYLE_SOLID)->build(); - - - $borderTopRedThinDashed = (new BorderBuilder()) - ->setBorderTop(Color::RED, Border::WIDTH_THIN, Border::STYLE_DASHED)->build(); - - $styles = [ - (new StyleBuilder())->setBorder($borderBottomGreenThickSolid)->build(), - (new StyleBuilder())->build(), - (new StyleBuilder())->setBorder($borderTopRedThinDashed)->build(), - ]; - - $this->writeToXLSXFileWithMultipleStyles($dataRows, $fileName, $styles); - $borderElements = $this->getXmlSectionFromStylesXmlFile($fileName, 'borders'); - $this->assertEquals(3, $borderElements->getAttribute('count'), '3 borders present'); - - $styleXfsElements = $this->getXmlSectionFromStylesXmlFile($fileName, 'cellXfs'); - $this->assertEquals(3, $styleXfsElements->getAttribute('count'), '3 cell xfs present'); - } - - /** - * @return void - */ - public function testBordersCorrectOrder() - { - // Border should be Left, Right, Top, Bottom - $fileName = 'test_borders_correct_order.xlsx'; - - $dataRows = [ - ['I am a teapot'], - ]; - - $borders = (new BorderBuilder()) - ->setBorderRight() - ->setBorderTop() - ->setBorderLeft() - ->setBorderBottom() - ->build(); - - $style = (new StyleBuilder())->setBorder($borders)->build(); - $this->writeToXLSXFile($dataRows, $fileName, $style); - $borderElements = $this->getXmlSectionFromStylesXmlFile($fileName, 'borders'); - - $correctOrdering = [ - 'left', 'right', 'top', 'bottom' - ]; - - /** @var $borderNode \DOMElement */ - foreach ($borderElements->childNodes as $borderNode) { - $borderParts = $borderNode->childNodes; - $ordering = []; - - /** @var $part \DOMText */ - foreach ($borderParts as $part) { - if ($part instanceof \DOMElement) { - $ordering[] = $part->nodeName; - } - } - - $this->assertEquals($correctOrdering, $ordering, 'The border parts are in correct ordering'); - }; - } - - /** - * @return void - */ - public function testSetDefaultRowStyle() - { - $fileName = 'test_set_default_row_style.xlsx'; - $dataRows = [['xlsx--11']]; - - $defaultFontSize = 50; - $defaultStyle = (new StyleBuilder())->setFontSize($defaultFontSize)->build(); - - $this->writeToXLSXFileWithDefaultStyle($dataRows, $fileName, $defaultStyle); - - $fontsDomElement = $this->getXmlSectionFromStylesXmlFile($fileName, 'fonts'); - $fontElements = $fontsDomElement->getElementsByTagName('font'); - $this->assertEquals(1, $fontElements->length, 'There should only be the default font.'); - - $defaultFontElement = $fontElements->item(0); - $this->assertFirstChildHasAttributeEquals((string) $defaultFontSize, $defaultFontElement, 'sz', 'val'); - } - - /** - * @return void - */ - public function testReUseBorders() - { - $fileName = 'test_reuse_borders.xlsx'; - - $borderLeft = (new BorderBuilder())->setBorderLeft()->build(); - $borderLeftStyle = (new StyleBuilder())->setBorder($borderLeft)->build(); - - $borderRight = (new BorderBuilder())->setBorderRight(Color::RED, Border::WIDTH_THICK)->build(); - $borderRightStyle = (new StyleBuilder())->setBorder($borderRight)->build(); - - $fontStyle = (new StyleBuilder())->setFontBold()->build(); - $emptyStyle = (new StyleBuilder())->build(); - - $borderRightFontBoldStyle = $borderRightStyle->mergeWith($fontStyle); - - $dataRows = [ - ['Border-Left'], - ['Empty'], - ['Font-Bold'], - ['Border-Right'], - ['Border-Right-Font-Bold'], - ]; - - $styles = [ - $borderLeftStyle, - $emptyStyle, - $fontStyle, - $borderRightStyle, - $borderRightFontBoldStyle - ]; - - $this->writeToXLSXFileWithMultipleStyles($dataRows, $fileName, $styles); - $borderElements = $this->getXmlSectionFromStylesXmlFile($fileName, 'borders'); - - $this->assertEquals(3, $borderElements->getAttribute('count'), '3 borders in count attribute'); - $this->assertEquals(3, $borderElements->childNodes->length, '3 border childnodes present'); - - /** @var \DOMElement $firstBorder */ - $firstBorder = $borderElements->childNodes->item(1); // 0 = default border - $leftStyle = $firstBorder->getElementsByTagName('left')->item(0)->getAttribute('style'); - $this->assertEquals('medium', $leftStyle, 'Style is medium'); - - /** @var \DOMElement $secondBorder */ - $secondBorder = $borderElements->childNodes->item(2); - $rightStyle = $secondBorder->getElementsByTagName('right')->item(0)->getAttribute('style'); - $this->assertEquals('thick', $rightStyle, 'Style is thick'); - - $styleXfsElements = $this->getXmlSectionFromStylesXmlFile($fileName, 'cellXfs'); - - // A rather relaxed test - // Where a border is applied - the borderId attribute has to be greater than 0 - $bordersApplied = 0; - /** @var \DOMElement $node */ - foreach ($styleXfsElements->childNodes as $node) { - if ($node->getAttribute('applyBorder') == 1) { - $bordersApplied++; - $this->assertTrue((int)$node->getAttribute('borderId') > 0, 'BorderId is greater than 0'); - } else { - $this->assertTrue((int)$node->getAttribute('borderId') === 0, 'BorderId is 0'); - } - } - - $this->assertEquals(3, $bordersApplied, 'Three borders have been applied'); - } - - /** - * @param array $allRows - * @param string $fileName - * @param \Box\Spout\Writer\Style\Style $style - * @return Writer - */ - private function writeToXLSXFile($allRows, $fileName, $style) - { - $this->createGeneratedFolderIfNeeded($fileName); - $resourcePath = $this->getGeneratedResourcePath($fileName); - - /** @var \Box\Spout\Writer\XLSX\Writer $writer */ - $writer = WriterFactory::create(Type::XLSX); - $writer->setShouldUseInlineStrings(true); - - $writer->openToFile($resourcePath); - $writer->addRowsWithStyle($allRows, $style); - $writer->close(); - - return $writer; - } - - /** - * @param array $allRows - * @param string $fileName - * @param \Box\Spout\Writer\Style\Style|null $defaultStyle - * @return Writer - */ - private function writeToXLSXFileWithDefaultStyle($allRows, $fileName, $defaultStyle) - { - $this->createGeneratedFolderIfNeeded($fileName); - $resourcePath = $this->getGeneratedResourcePath($fileName); - - /** @var \Box\Spout\Writer\XLSX\Writer $writer */ - $writer = WriterFactory::create(Type::XLSX); - $writer->setShouldUseInlineStrings(true); - $writer->setDefaultRowStyle($defaultStyle); - - $writer->openToFile($resourcePath); - $writer->addRows($allRows); - $writer->close(); - - return $writer; - } - - /** - * @param array $allRows - * @param string $fileName - * @param \Box\Spout\Writer\Style\Style|null[] $styles - * @return Writer - */ - private function writeToXLSXFileWithMultipleStyles($allRows, $fileName, $styles) - { - // there should be as many rows as there are styles passed in - $this->assertEquals(count($allRows), count($styles)); - - $this->createGeneratedFolderIfNeeded($fileName); - $resourcePath = $this->getGeneratedResourcePath($fileName); - - /** @var \Box\Spout\Writer\XLSX\Writer $writer */ - $writer = WriterFactory::create(Type::XLSX); - $writer->setShouldUseInlineStrings(true); - - $writer->openToFile($resourcePath); - for ($i = 0; $i < count($allRows); $i++) { - if ($styles[$i] === null) { - $writer->addRow($allRows[$i]); - } else { - $writer->addRowWithStyle($allRows[$i], $styles[$i]); - } - } - $writer->close(); - - return $writer; - } - - /** - * @param string $fileName - * @param string $section - * @return \DomElement - */ - private function getXmlSectionFromStylesXmlFile($fileName, $section) - { - $resourcePath = $this->getGeneratedResourcePath($fileName); - - $xmlReader = new XMLReader(); - $xmlReader->openFileInZip($resourcePath, 'xl/styles.xml'); - $xmlReader->readUntilNodeFound($section); - - $xmlSection = $xmlReader->expand(); - - $xmlReader->close(); - - return $xmlSection; - } - - /** - * @param string $fileName - * @return \DOMNode[] - */ - private function getCellElementsFromSheetXmlFile($fileName) - { - $cellElements = []; - - $resourcePath = $this->getGeneratedResourcePath($fileName); - - $xmlReader = new XMLReader(); - $xmlReader->openFileInZip($resourcePath, 'xl/worksheets/sheet1.xml'); - - while ($xmlReader->read()) { - if ($xmlReader->isPositionedOnStartingNode('c')) { - $cellElements[] = $xmlReader->expand(); - } - } - - $xmlReader->close(); - - return $cellElements; - } - - /** - * @param string $expectedValue - * @param \DOMNode $parentElement - * @param string $childTagName - * @param string $attributeName - * @return void - */ - private function assertFirstChildHasAttributeEquals($expectedValue, $parentElement, $childTagName, $attributeName) - { - $this->assertEquals($expectedValue, $parentElement->getElementsByTagName($childTagName)->item(0)->getAttribute($attributeName)); - } - - /** - * @param int $expectedNumber - * @param \DOMNode $parentElement - * @param string $message - * @return void - */ - private function assertChildrenNumEquals($expectedNumber, $parentElement, $message) - { - $this->assertEquals($expectedNumber, $parentElement->getElementsByTagName('*')->length, $message); - } - - /** - * @param \DOMNode $parentElement - * @param string $childTagName - * @return void - */ - private function assertChildExists($parentElement, $childTagName) - { - $this->assertEquals(1, $parentElement->getElementsByTagName($childTagName)->length); - } -} diff --git a/tests/bootstrap.php b/tests/bootstrap.php deleted file mode 100644 index 902b4e9..0000000 --- a/tests/bootstrap.php +++ /dev/null @@ -1,9 +0,0 @@ -Q&;>$6Ak<+p&z6dpZOqpeCC7X@R<*i O!)HE79-sLjd1C+{z!h`= diff --git a/tests/resources/csv/csv_with_utf32le_bom.csv b/tests/resources/csv/csv_with_utf32le_bom.csv deleted file mode 100644 index dd67f852931d68e971be865afdfbeb20206e9725..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 288 zcmezWkAWeXfq|hIh|7Rj7m5vmGzjYeF)n!{0`kT{c`h9KL3;6-50b}cK1dFq`5-xb N=7Z$%nGcc&VE|qo6?6aq diff --git a/tests/resources/csv/csv_with_utf8_bom.csv b/tests/resources/csv/csv_with_utf8_bom.csv deleted file mode 100644 index 216a017..0000000 --- a/tests/resources/csv/csv_with_utf8_bom.csv +++ /dev/null @@ -1,3 +0,0 @@ -csv--11,csv--12,csv--13 -csv--21,csv--22,csv--23 -csv--31,csv--32,csv--33 \ No newline at end of file diff --git a/tests/resources/csv/sheet_with_empty_cells.csv b/tests/resources/csv/sheet_with_empty_cells.csv deleted file mode 100644 index 157a854..0000000 --- a/tests/resources/csv/sheet_with_empty_cells.csv +++ /dev/null @@ -1,3 +0,0 @@ -"A","B","C" -0,, -1,1, diff --git a/tests/resources/csv/sheet_with_untrimmed_strings.csv b/tests/resources/csv/sheet_with_untrimmed_strings.csv deleted file mode 100644 index 4be5db5..0000000 --- a/tests/resources/csv/sheet_with_untrimmed_strings.csv +++ /dev/null @@ -1,5 +0,0 @@ -A -" A " -" - A - " diff --git a/tests/resources/csv/sheet_with_zeros_in_row.csv b/tests/resources/csv/sheet_with_zeros_in_row.csv deleted file mode 100644 index 6078d9e..0000000 --- a/tests/resources/csv/sheet_with_zeros_in_row.csv +++ /dev/null @@ -1,3 +0,0 @@ -"A","B","C" -1,2,3 -0,0,0 diff --git a/tests/resources/ods/attack_billion_laughs.ods b/tests/resources/ods/attack_billion_laughs.ods deleted file mode 100644 index 0c29831fae33a6afa595f431e32ae48b053a79d6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2681 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 diff --git a/tests/resources/ods/file_corrupted.ods b/tests/resources/ods/file_corrupted.ods deleted file mode 100644 index bf012eba337cb4bfaf66638a7a4a09db5e7034ef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1735 zcmWIWW@Zs#00F})d3P`aN^k;cU)K;vT~9wZ{Q#&k4hFvjdG{^v+GF1WrJpk~Fo+^6 z%T3J7OiL{;(W}VK**fW9)?ovlw)eGMkE`N{CpLSx{`$IMz1)^0#v%cs#MSC4 zd*7P*vV3w+*?7HMOX1P^^wq~3bC|y6a4Kz@dT&|JlOTIHm5ukGzm>V08)C`*`vr4Y zYT1k{LDJ4Ej83e)>Zbp+N-IJADy?pxKoo4@6-k%{~ zzqy(nHGE!~`dngSWMGg2h7SiYd~#Du5!p+y6gr)7$-cZSDzitN7^Ppov9Z7c^F97i+#e{!-U6 z;8C+ll-ace&;1uKCvVk$^D{=9B{%f`B2Le4$4R?)NKFqk(le0TqV+PJL)PHh*L9w2 zcN?sgFta#nE4(tHyL?vYrF)VaE?t-F$+_qpkYH_c+O48WG3or*o)7oG$W~0&dq~=whH9c$I64v*eo9_pEQXH?5trT(`)*a!<&j)B{?N zADKREPto^NVxQ+&R`F2ALjQ*TyR*mkf9g(UOPBuSzVqklS7_nXnq%3e0}LNMAO?q1 zCNQKb3sPARHgYm3aIhTc%~8A9-V)@Q{qLPc@yvqg9~J9FV!}i?uTR+h(03~P8q>yq zs3D)1v#@j(Fw|Rs#&H2nEiS3dNi7D4eVA|YYy*M4&%+H~NebWcKFpGCb@}DS<=@y( zBpuB9a?s<@vA# z{%>l^wZN+Z3q`LyVVbt*jr_dQ+eK`li*G78?!2?Bn_;%@GnI$wlM*>oSGQe#@**Z) z)YV>M{(^9^-frq*M_OdQ{OqZ&bk_{Qx<)8Z*}nM z_pJS4Pu8BBInB28kAi})@>)69oLifeJ+jIt882R&cb6})Rendq#72#%+$Rbd2Ua`E z9k^r5URsi7n0s*fe)FS;>YC;Y-{aoDGSC0+-5bH%4=!5V{yTa`b9jct-Ma}rz8e=m zcxC$TeU07wbMa|6r~3Q*AAO^mACzZ$YR6KZPi!+b{BGx&pMKu+OX|PcSBuo{^DJlT zKfaQiaZ`+mpf z3by58X&xW_TCdK@JnPwSr_%paJY1sWURd8L@sz7C$}?Un#N1k2zMt{ay$5W)k1H~_ z?6L~@c1zNB1zV_!iJVmWicPkrJMK;RV{+73t4OP&eLA=7oM{>sVr)Lz3&qxD>2%uL zp5yQA=w%oDd`stlxAN3f`QIJSWK=z`PAj(JV>)*9Z{SXry4nc)HvbQJ-F{Oh$U*ws<$~ME`(|wkx$(W?A> zf{kLFE{jTDtou4Y@jrinHzP0w<1Une$r=eD711zl*b8Zd9(5oSt_``!M%WB70=*nV z7~u{q-C(5}x-RrwjnI`3^j9u4kE82C&$$R)2Z6c}`8dFvl?^1v3WRb%S4?IF@c>S^ BxefpT diff --git a/tests/resources/ods/file_generated_by_excel_2010_windows.ods b/tests/resources/ods/file_generated_by_excel_2010_windows.ods deleted file mode 100755 index 955a465290a962068f3dc4231efc35d30eb9414f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3281 zcmZ`+2{@GP8Xjx5tRs`{NBA*z5<-?n_85Ce!^|*ZMn*zm5Q8koo}rMfl4ow__y%|*VBX%IC|8&-3WJvOL&Ie;Ff0lyi}6OF;TYH3UI?_WEY{lx0fS@R5eQ%N zv&xn|oFKP3-pwq3a>ln}@wT7gU!a=b5EZUTZRr&3p~?Lm?m$iR1|}910)UKLt&g=@mVW$_qo(B_!HY| zZAH@UlpZn9K-rTW{j4r$-*d@o7`;BP=e#1wl(EYfD4!naFpWZwcNO&a`>!}jHMWdS zPPL~Lt*17Q?&SnPd+vAFbVRn56~S1uqKl}{clibo*2xD;6r`l}BP1Ie+q01;Wlxh> z{G3W_E2RvL)T)i&p=$bu-NE~QSKbwHr%lZBd>J|2lMmquadfW)T5|20mxHR)IptDBp?m1 z#(01{>g=U>f^P#OV_tA9|KJyDU6D&2Ga>%{eMv#~I;l!|kliuGM0uFIsWr5MaQ3t> zL}j~gh8QbQS5--y?H+L#E&a^v0ryUcYP-0+wBUKa5G^~G9tR;?0-3iY*^DYL6{a0j z8t&i%YSRcUf9DPL#=x0DV(u0__wYTIC2%<-JK1!ccwX6{fDPcnw5b8$MOGyJz>VN) zpHjPmX?!VWCV8W8eZqrNRU5<%wZLs!&{aD=h3X2j|ea%vSUG-|G zq>bJtqPTHO%vq+7S{@M%^vE+_9po=2Ylq$7u280HJ~mHY3q@Bp)t8J-%6h=@j7qp^ z&+2{l7u6Qu7z0^2q&u4=CM$=P>8{Dxj+4nZ#Wg75in0sHAlsLlCg%qfYqVDVL}N1< z*w1~1Yd^?-qy3h3T=~PU#9gIu>^QO`R$OI7txeJ4g-1?8;v=umT`EdOcZLDF0}`qw z^9mK$<_1XI&oeF7$y1U!ND*!tAk7#luPT`-=PmuBKbLKuRY6l;*BU)YsetNhjp4vD zHHS7Ymu8$euPP)rqE;(gRgr_0CIvvWZnadeuT7y#YS>5tH3^-z`3ke3E)0Y_=qUh| zTljTs3>g!z5*aH!-T6C6v-h-k;z)hF!f&F|uwoI{ko^0uh`_ZzkF0SnkQNneas9Gy z*z9TuRl||+;^5IiBE^_J=92Vv!7guw53dps?u~gAY1&-K9E*=v`e`Dp@#zPuOHBoj zZ9BfJ4)#GWc7ik3=LLD5wmXh&rtK=Q#>0v+>h#Md9`St{QD#%M!O!CoZ9*F2YMYY9 zAh&*;1HK}20RTrwG=Tr5JgeqJLn18z@bL@)zcBMb<=SRn zRm~(xJ<~aOtRu6Gc#n;%Y1A zXEJ>#p-g#{)2#cNfL1fIt%#zt&WK?q(8yNWrI_#P-7;3KshJHM(cJMT8Ll=)CJWx3 z>aOtu2MKP}aig=%L^b29J>eY7JDXOqn)jet`P3Ez#>Gms4JFH+l^=W6CvG}*!TdD@ zi->zx-A*8x^xnaB`rA32W8JB#i#Ad)VQ+a0YnXDJuAGC_-kC)^Hm>3?r?pmaRlGzd zpg`^Qmz~iU(BK+;Rx#?af@F%pHc*FAKvGv#VIC$0zkZ9?Q{$$kDA-VvCJC_MQRx$} z(rXX^4;3S+`{!y*zhU`4U-sZ;qQ=6fUb9;Jcl3u82==YnHB_Q&KB&ByFL}q3$RFOF zpH-d>bt59V+A_UcETO2R{uqvAht>Hz2B_E7>GEIi>>S-=7`$3kJFwE1{bS6g$s!q* zpJE5d@7Fekei(#KwtNm&2J^j_=|*PUAmtRwZlig>VO96MN;eoL(FuDk+#0BLIQ6

D8h7IxW)mGe(iGL;4~Z$E-H-63syq__<0Lu*Fb~wb6^ys$l%E$Z zHEwtK=zYC4l%$6?y4CoN!*+^Qo&5G=^RoSx>fx5ZFs1+(1$9$f%9klb(5Yx^k{dcU zLX{7lgIkhE2s_wzJoocg^oB?8>l5j8bpd__=uQ($ z*p(4|7N#rgco7*mVlbDpY0YWSu((K`teN_8vBwT$^^tS{>_TIq#V&!Z@fU!l>f_s4q7 zn3YSaS4(;Z1(ORk90Txg??7onavG_h42X#oRb6&$LfAUa&YlJNOl(%ntSP7p zbcOWYUt5N+e&&0a*;HD$cR+BMaYNIFuHi-*St|1Q(uM0M%$*@y*X!-xWQ-fs=|n;> z#%W+zjB)D8Ed}iQUdAKq2p>7P^TBJ&>Lpz`gD?PgMDgA|pfFW};Y zkx`Pt1rkPLSds^r?MjNoo9@8sTd$a9f7~m0XaDTL13PUpYkODVp-hp560G;>*7mBVQLbp`XIM!Lqo{>;^hv3!rj%9JkAE7S`y}d zoce#tb*NZ^**R9M9%=x9`}k=JvC@$-G}D*!f}v4J1oo$fy+~^F=#Xc>yb(bOA9C`+ zf<^p3=Y|RnTkcams7BKc8~Kk5Qx2r)E|PM^LT5xtUy=I5f!LT_^^dJu7I`+yhSmm7 zK99Bdw#(nIxLKD2)EJ`6EVwGBsuI{%85Zxt%fCO768(snetJtN6Q}7A7Ufw`DS57L zG>*e?4b!A~_Z5%l2F;2HU!jLz*&!&bUkI=NZpDXxC}2==DT&R8wV6ip_R2Z6?*gqq zw$5jrq9vZXY_Yu^FKwx1{U~ooJ1tWHca9qyfOK5gFYQcw+E;+W`~2x z^&vx;jp@MP4`4mH{iRpc&c|u^tHh_ct)v@M0RZ^pME!J%2w&KVzQ;lbu7=A4n=^MW z{bMdyCUe>yClEUqn{JvsOZcY3XWUQ)Sq_l{+Ierzmj}HsB`+;a8<#whD>LJD5+bkz zL(dc@?OWv>uB*-EbO*cyzl@Ef4hq+s{zj3#K+FTSN}VI{Rwdm0sCbPWCX2U>@6* z#Ur-;iF>ZbYgm>#6lovX0fa0(u8fh8Dk9w_E3%z6%W3n$c#tdwT?t5j-v(DwqzI@;h`W@C+EcGtF9R^bhg{XKUJ^p3kxG(Jo z^qW8alW;0(nV**VcmeQNbAGb=Yy8`)n;V~?{@#B7LRcNY{xajg<9_eqzi?NMJNtii z^xuKMQ|K3vit%p<^?Qili|bbispAvG@sZ?bshJzo(Vy(l9bZDnrI>kQH30tvM2Wi_ diff --git a/tests/resources/ods/file_generated_by_excel_office_online.ods b/tests/resources/ods/file_generated_by_excel_office_online.ods deleted file mode 100644 index 6c3f1f0db0ff83d3163140ccf2740d7cb9a058f4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3054 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 c2FL0+dHs6f7gRg#xh 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 diff --git a/tests/resources/ods/one_sheet_with_strings.ods b/tests/resources/ods/one_sheet_with_strings.ods deleted file mode 100644 index c3df611ba1810b2148720017b8421ce6db4ba3c0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2561 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 M_&)x4Q$2k8AH1^?od5s; diff --git a/tests/resources/ods/sheet_with_empty_cells.ods b/tests/resources/ods/sheet_with_empty_cells.ods deleted file mode 100644 index b7c3a91e89a2e8a477307a144f5855c9cd25ef06..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7473 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 diff --git a/tests/resources/ods/sheet_with_empty_rows.ods b/tests/resources/ods/sheet_with_empty_rows.ods deleted file mode 100644 index e7ad29a6c7059191903d71ed0b69aae04e72aba0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9253 zcmdUVbzBu&+x8ZurKF^lQo5wOyF)=5Ho4gw*fg?{mM-aT>F$#51}W)oq(zYr&Ut-2 z=RA7e@B8z+_ixs$nLXDvGxu6^ueq-^%5w1caRC5i0H8QrLBxtf zYoNW04bafW24rDuXm0@lGdqG!m>`Bw3n&xB1_(BR7&}-4!S+m08#|z(3Dg`2v{(Kc zXY`x7rlni5yXDa>r@Fa=wGr6R0t98Y|50VK0hEn2_Xq76$KeF)f2L(l#eMXpV3lNFg~NEr)Qw2W#wRh z&dI^c#lz3b$-%=Vz{kzO$IHhrC?X&%DI_i}D##};C@L;2s4OicCMqT-DJv+0o zXl$%*Zf|IAtNGB`{Gp?%y}!MoyrZSQ?L$+4XLEadM|)RaTkl|hS6hEiM}J>m*U(ts z(0I?tSvoul1@&xc0`2S+D|CznSDyCpUR|D_ez`pV{{8!HU%I)u(Rber000nQ%7}}oJJ0UV-YDUiks#T`b!{uRZ=d@tjmomXQJ<$63}QdVtr51pcRzE;0=M#_fM&PaAgE|~UdRhvwn&)MPn*ul7r-_KyB#{_IAvg8d+T z&BKI}5@2jp^cEWj27;05P*?Xhwsn~TUGai|MV^6vmY(bzUboD#OID68p33Hu6He0O z&2tw@G{I3WrRT~-l&V>qU7Yb`#2TLyb{9$?=emn~OBq#aEfj=*EW}!^mG|py!#I>> zYZ6)ABSP2-D-cj?`X+8>jWwRSpGT`$-fS&w2*x&qJavw#wN|;ETFe%Z z;d?D>p%9E(GSkCbHeIxB#WBwwPr(&dGz#*;Xj1irmABh{-(FrU*Jmlt=j)x_B4D&W zo=eQNK?pCdt2(CMo$a+sa%iEU$E?tv-Ei=qE2irRLD$WOOt;SC_`07}o2bJblxAFh9c|OzH%FzS5d5aV{_OMe<8!Q6Eyl%NSz5B1ADt$v zKt9isGQ%BhCSx-Za&)84Tw|0pIWM+|jS^M5X>Xg9inXXF@6&LlbC#{s2Y2QpD0i7n zf?-6$BRA&l%WpYuky45cod-6$LcVf-cw-637KpRG+$u(7Xi87V3SPWmo{`KDHhRJRwbGTS%Z|t9p@{zwSZNg&ZNBuk8MUBO%;GV`iArHnY9K`%#UmsCtzXSdHU=3 zYW*hWMO|F=z3}NcWEbzq=ZsnOPlowWk-gI1!Dzzx3=fHAj+<{BrYRqy=%K9gZru=E zk0G68`tspqvRnj!HDL|&z0opgBxe0d)k4$Y#iiOF;!t8$Z>G9W#LwNnW(iHlE?~H7 z%hbf+qcyAcbxZkCyKYQ;7BO>W-gclxKMsK=evL-V!j2X98fU??YhIM2%ZQMwAuD9J z98uDN2;7vKQ_T$r$L4Nydr};LK-Z9U(#G@RaWsD^$APkggo*eTHy=L6;bSzUXuq+3 zfo=k=i-X?C<@$;E8}D^lO5DQc_lUZ%>$(u9HCh>`HOsZgg-kAoWXe_L4R{pH7uY&&-cmwHvAmv93ntfO*yL`q6xSM$tZNL8+Ny{FWkTGn(q(eS z0fDFfy6Zvi%gGC5m=_O(=?v)ov)7X8-CcvmrcB>tH8rES-BU0rl8Ut5 zDiNCTBeUHvVATYJH&PxkY|owT%t2lpPtI+=sp5Q0B_UXvR1rD}caX01#nM1v05S_H ztZCOTwyDb^s}*!r0q9gwKg-VTiP0O%PDIBnl(r!V$QoOx0&LR_cVb<_;@!CC6heSX zDTM1v@K#0YTeFTN-DaP^5C%r?>*(ptZg0Wz{a?$x&`l0GpX_NdI)nAtyz<)?sX;pJ3%fzeX!}+J zsy73ymFM1;#5*E=(HxD=;|=8=K0K&b7%}i+(lBv&6O}8XLJ8vcS-8=bNr5G}$$Hr(BvU0Vn_LincHzZ#ZjRQQb=KJ~Q(WDbR z*_s(qqNsfej5g?z<*#GAaq3aOOR49i?E;sF!*M29*>HKx{bVqB<`-OQzFG}_yy|Qw zdfVqCz3+;9MB9b)xLGji5o;t}KnxN5R?V?m)~P3k>70S5{Lo{4C^2iKDlCJZS1!Lk zru%|lWk^*pIUHDH*CoTamb5m4NJ*OvaYMa@7b=gi{V^w+f!`m#G!h>LqIa)Egf#+9agu^ zqMM~BVKWd--_G&Trix8lI-xJwLw9Kr3GE1Cf1yj1gY)6=Y+1PBTIGgTkZT$*PL%T~ zZr%sBV(d32j?Fd`*QANUq3(0=%)=GgJtOVgP~JvS{_^ZMI?{qsqNaXwUCxYe0z+QX zTe>=GiXGLmL?LbA*5p)gox~$H+F1f|Ls3pBasrG*5ry6?QOcmSgw^I!sM@$(l5a^K zhF_{FNUVXZ*0_(r7wDGNG;KWr#vO6RpHP@Rx;x`+Fkha`C2tRgd@#TeG51q7CVjH9 zJxFGZg&Hn%K;|x_K3hL4^tz~`zu6e$22NQH2|1rn2XFm0y+I)b0Dd1Hp5BHBF$mby z!py<$j}#2b#%yS83(-m5AJuv zhP5Hs!W0O#XR=nC#d{=tNL?eTIxP&^Qvun-Ug<+>^a`q>1ulN1k%rz-r1>i z^-f?q9{m_!My>~-;fjoB&Iu>{N{tk)liaq_54tKtkx|G(L@mzNUK8;Qt)Q5`P_f@l z3wODAED$u$o0nFOYFMq>cg zh(2_9dg1MZoOQjWrqTl)@5R^A=B@|Et`6g!pBE|L86>eCDD5=xx!i1iXBxgCTq&t^ z6bA_pWtv@uZ&n_YYe<#c_5aEX^D%^b0D$-HqxJj83hlPdj3HqA|9N!gUn)RWIkB29 zUOb{3Ay(*LxF3+| zV#q;Vj@(;yJ;Eem_n8^tR!>I+!(&Jl1vx2?{#hWW8>{xbr@J*gAG_v^ky|~8W1#~6 z>uiA0sXWG?H9!WvtxL4N0%oMzVBT9U}))PorYgZgaMrD4!MOB<^RnGGDwSdD| zsLiM4KvfifStC@qLUx6A3+Y!NM8A-AvogW##sSSVNq;{=$H+-hPf29(6#fRcShHeD zbM@}%WR)(a56=P%NBs~NQ}3J`SD0%6S9lqGTnjo4|CHalf6uc-pVs6BWdvXK95i{}<}XIo z8>BTMy&X2YrDOQ@nlg6yf%f>gbI4>y3rBNS?41b z4(b^{7)FoY?~1SRWr{kTUKvdftI~^u*12CtX%m(i;~v( z-H$APBn?+K((hm2?bIK2HGd3y08>jcEHeCReUFgft7EdBCTxY0e#sSn*?cM7SQ7E;A_d#bk#PN}@0Qb^<^^k_PIHyR z6|y`x*R8=PcJ9RC8vqL_m>C+9c!UkHaB*}f^zOWURD4-z8KL_Lpbse>YVSP7aM#~> zW%;BN{3k{Gp zeyI?L00xfJhZ5a5c?uO;Ub1%O>zyRewry{74DLUeIP2-k?^l7Jybm$?@L(xPzw&L? zLSc{$B$m^x@({cKdobVJd1zdEQ4Pv8GMUy5hSUBhvyA&j=Li&4D$W2e88=T}N-;4_ zRR&-NO1yv1d)T2`1sP(^H0CUSEk^X0ukUp_H&-%->1lSmC%e9Rs0MP~B&#;Mn9+Wh zrX$x(pE7X!Iz*z^x!E$p1zt?V^T=K|sffV(_FO$}KQ{3k5d^UxTd>K7zqw+%LA;At zuTl68UG4(_ocRA1vF@Z%sJ#ma_(zs7uVo!J!HM;KK_A-})pH^#N;hrKt|m7h&yrgB zZ7lhcTvA~Wj#RbNMX3H_8L8U|%u_WT?&;$cv#wP$J${tfsTyjuP;u$Lf&!Mhx9xA# zX@|`uO zO+#M!P)u$guhnkM%l)S-UrCw})4!~q6+ra@?6&oY=F;i#D)nB=2Z48?Lrn$Rt@sVL zPI%o);qKscrHZ4dw7|Fc{K>w#aoH)hdZ7`BwfFB)P)H>oV=$hkKV=k>@S4zhb|~H4 zICGDWUO(SWqkLO+QWc3&JSIf6HJQV!+&3d2^j#8}z3UAB4Y7&v{&$Xo@D6F?*C>Fd zO;dVQ57M=7%E0P92Ew&!8ZS$1Q%UQ&vXP-jT`g@UQ2$B>dm?)30gXhVvG;^SnW~bN zUR0}R)!FlTuW6?&8F0xLo-hWT7Qwk!%;Y-67q=xh@IWzYOheTk0;zy@P8NJA`WSl^ zj>mN06qFyt0|tBU$@U$5;!St~lSscH+VHxljifXsTS@3A5J=K};2mla5{9aJ52);N zES|mh*fL*rOF`;E=ypeacNM1dupIwN1i=6Z_7soyaE?@AbiQz|uG1wxf`t4f`yh9J zh+K|p#yb{#Ugr=5ru)SgD(KsyJ{c@oG<}1TBe!UdjOMII~xST(n&AyzKdZ9QrZRfSP_<(UIF+5^UCQ+|B z*S$dRbcz@!7Q4+U)=|;C%U2PX;VERL6bUsUH&QbPgf`M|E)wnI%1#_IeaboJjm0v* z5MmwOp|sF-fGaih7}xc=`igKHHoVh1y4g@7IEkbhuV8+7p)4gJ;6&u(*u#qVrs3^2 zoVB8$#Bn5Y?4hxhEZ-&n*$3wv>WtO(9SrOANwVOmyx{ixmyd^yFS9Q!L6&<=$9#$1 zv71nLPEh?8Jrn?>dC11cYmRHccv4ek_3uP*n6v1B475t8HDW4}xQ2Newj43>zf z9oJ9_OEF2cFl?KBpphw};md9?MT#BD(jj&jvmI|fR3J(fd32%&=H$K@a$C6$?U7in zezJ4_Q+A)ca-rL~KyusGebxus20ic;OA1lk!E3>$IN>;hr)VKVekkHmaHeV}htQWw zKKO2NU1`-7%+zLQ8hiEuq?$osA9L4TA(W5gYRw7;lKo{8>9m#7$KE42(u$B|TW60a zFQZWA3*fHo222VhLKHcLzsziD#61+C8IA!r9G#xBNAuh;#i3{RzEIg|EAvrp-LLUM zJ|N%!PD92U;OY%L*?Ofrk<4u+z{&c$8#5o8GvyHPLz$MYb`|{y11p*zUVw(e0UFv% zrs$O7%4@~5iPfz4;ncSp;#KN(fk#mJFbGaOcCXM3#)O%jq*D^o-KSt_9?@t{Z-v6yu9WngmJ)v@GZXN3m=G%cPl-j?bOr9kl3O zUPu;&9yiPnSA+!4xq8aZ%1Qe3!b=OE9EcSje693KpWu)bIb$>sRO?Io!pR5^JK>Uv zCK;A~j?eHMhWmU5kys=kyXMKTce&O?%!cI5`La}f?wN~|Mf2Iz;G&(ORqI z)oeAy(m=$rD;X%d7&4pAOvUGHVjD>J(QkUQ8=iuf&ml%$;lj}$B}5*>iUrP9*<_(L zZDMsz>s^Nm+!-pX0Sn9;^n@@~+uXIBG0(T)D-ywKgK<#{9F2txz_H2yVx{bL>xXm5Cz zpN2%qg(b4!`d)m&>)9`6tA0XdfNeUZA*E))HnvAm3Q^TIlInUe?xBBT)5ebv^bKoq z8~)t6=2Wst<9u)YqxL=)8ZD|Y3Zs?7Y%tDTPPvI; z)38_6-vbe6y_z!3T#QO$3#y7GtgNtu>lq6WOGlE+!3O3J5uS%km1~VpLa;s-;6~Ja zM!~IBpT!m0>HE;mZ5`?J+cy=SOhy#>E?bMP+?e9MPItF+ zQ%FH1D@~0?5=+7eDg}}Cw`H&+j}~lZcV|#v{TuvUgF|9SVjBL}#0u~BHAO)}U4&6a zQIh#T3F>X@7peDKwzFV0Zqf%Acd90;vD(@ppQ4RnDz8^jhHHv8p!P1lz*tiWl>2mE z9X2|^f~IX9x}bq1Z317tA|V)rI^r|5G7@+(|LU5KbGNh4s!{-pD>j4fFud5B-mZ1$Pz86x1Up3#;M$S!( znK=yRA{ULY@>)G7h~qVQI_$UJq$I=Yd0>wDL3FUJe9r8~bCy^2H7&5iur2V+6~J*| zmWRa>I~UI2qR7Hdq7|~M)iL*`iPVnQUra7L64^`;F1=a(~TQ5u8 zz7Q(Q-Gjpg{CZ{Rd!$^v|F@X*8*S?c>5p#}ev$wHcM{4^Tlxj*UzM%DyYwgS&+ij{Za>2K z?~D7Xbp7A5DE@-wr}FiCmcQSV&o5a1s)YTX=S~azX^p?&`MWaqd!{>W?5Caog6XGH z_Is8)t?Z{UVE(s{|4TXhJ=x!#Wbq5Kzba{e^8Dz}f0c>;-k;NNdHxf)Zk4qkUGE3- zuj$&IjP%o%u>K}0{S)}t5%Vsa`)QH4QS)bR_fOnk1MeU5?tg&Nx6OO|jm{sV>pxL{ i4J3C#`KR%a{U$^!%OTtj=l}rr?a$^m{-plVTK@+>#mIsH diff --git a/tests/resources/ods/sheet_with_formulas.ods b/tests/resources/ods/sheet_with_formulas.ods deleted file mode 100644 index a5bce1f1ae67dbd9b406c545841adb054089a046..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8450 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Ⓢ_ zMH3Lr=sGtHk5PaoG2^!FT;PXIGDt2)B9X}2^0UYEwx)FUP-d0Ogi=L57CGBuxK$;; zBpShYF}pn#_jboX!Q<=Ac1LZSJ920tLgr8)9N;y7GV8!nTH{zf#=k?s+9YNN)PdDbTQ z_WT6ax&QRuNw(h}Ye@WF5kYFk^O$k$Y4bZV^wcm3G)iI_3uTaa z{HlKx$sA-@xmah^SLx+RTYMJU=)I&`-_7L46HRRDVatY{HBo&5M#fpPMu%X*4AiOq zt2Ob|iEgBl-dDwicuM_bV7H)v2eAh-!lkrnJ3#H4pqdNh=JzbZ&Ux;)kOks7dMXRq zcZt+PLR%w~vJ4(nIlH^g3cgCZppGmIx|mqYd&p#=6_>$-@B4|5*Ys3x)LuHT&Z^Y+ z%%ME$C9G8bxJWO2PM5sluz=0$#&rkJ2BmWsAd(pKe@$ZUAr^EwaaCbPDfwr>|0aWo z-hQvzZP~(%Ter#(R@kPTr1I3x9_=gUAdccnIc1c(NG(R^oCfx?Qi#m-QAOmyXJ$-o zn}}I8R7q2$ibZjOP>g>6-o^fq;~AX`dJcG7msJ@*F6XNZ`kkmk8wUGkYV(rev?HP0 zNz`H=nG!Z{RG)@`d!@&=dp|0_BOH+{s>I&d7f7*P6X4a5=s8z^OZReWT-3~=Cl~E~ z|5F|-Ho^oRLz=!9EA+EW?1RBrR zox_JXXoSNnHU#iB%5SsW2MDazcq zi4XX5^$}s}pX7Tj@>i{2>x(~BR1wWTRU5yfe9!ZLW`?c_;UBV&s9XL_5C5upedl-b z=bAbCA!mr_?^ojH4?NO$(C>xVA0Pn0HC^#Tln`n3ub@A(NdGkH552#OuRqR%e}nUb zRrfADF6JX!{4C%%sTxU=bCr=A&iKg`_(r8WTF0ybj?Hk5VPMP{a~g3 zjB?FO{g9O3p!_eE>d#>RG}7d6zJ$}o`0rf5Q;y^-P?$|{!c8{_o4S)^XJ3{ z(Jw!V681j`oL?1xULmjZo*yEP_j4BXtKQF!_b)2-zl!X{h*$m3YWl0z&yM8UDgO|H a`+soJiZaNExeNf@LHr;H|KtAG==mSpY+J7Y diff --git a/tests/resources/ods/sheet_with_hyperlinks.ods b/tests/resources/ods/sheet_with_hyperlinks.ods deleted file mode 100644 index 7246db5580e7ceb2f85e502efa60baa0d53a6ec9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9905 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 Hk6H6SyF#aN diff --git a/tests/resources/ods/sheet_with_invalid_date_time.ods b/tests/resources/ods/sheet_with_invalid_date_time.ods deleted file mode 100644 index b823ee5a6971ad51be0e9aa1b1355c4d34438850..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2582 zcmZ{mc{mi@9>>SnvsALLb8XpTD0@QowJ|~rH-jO}jP49Il(J753U7?Z?$mwnRi5{r^PGRqIp5!Tp6_$MpK~BK%q;u>0DuGV7%goPkYw$& z0t5i^H~|1I#;kV`Iv9ZtR>h%^Pn_SON5RLM=apHg{$`3t&YYhC8M%RsvTXYP$(89g zeBLq{q-ePR+~#hLVuyXg0K3>oN7pijNFU#Gjz{vmt}7u~syE4t1iMpE&r07sB?fJB zb(VU2bjK_`l-949xpga~FtcD5G7$fnPtT%ZNqpb8EeAwR;?~uJ4KzzD&j)ABH)?wo zbZI;dL$1*cUitGxM;oG_k(Emirr8cY;~SB{k_ z4h591F5>~*BU+mXalSQC*#_AK^*`x^DPPI2I0?<~kB#6rcdHfHvA^^j*tu+5H>N-m z)qYuebJ>d*#w#84Aero`-tuV{*wr2a?kF9#lS^19o{y2SqqDc#^i5QZZqcX5w&=D( z5-@jFI7g0;lb0E0_*jGiSYE3fsPAsojqSfMt~o?pxMWyEGPA$E#;Bu5Fr8mJQ@bDp2Zt61bn(YEvUa|1C&5Upqh!)e z!+t+sZJbaERW3e?5`aXbTmZbu<{zi@IV?~14f1#8PZeGugcNkb3%rigOpj)PMYiC_ zsq`_Xne}b-dyd!+c*IBo>CSyt9XwU#_~KPKmxyRL&4Ghz6k8k7}ch<1-t}v$E}4Q zq1~8X(XvgcGU3uk7OpaxmX@=@w34Bj&vH*MRH(N@zIgr*q z>6ayan>kCa{kB!{RIYw<>D_mkDd>v>Ti#_9R@-boxjYsrWc3nSlIY0`b&8^UfR>_7AV#t%UW7O$l6T zi)L-ZIS3wZi4Ttv2+gS;dP2FE5Tx5?3RZTY9C=_OQc9xgkK&`s9u%I!ScEoT%tN#) z(WW)0@IGqx((F0L*KkwfI%-2|tKF|#=mZ3tpBUYu;p@AlLq5eKb>meS{-`p>PoMl5 zPK91Yu(m~V0ze!rt%*qa5BM0URp9=r4mRT|>`(8V?XX$kW8`@k@RG=h5u2ARetLb2m4(~vZ@k(9Hrp&; zyx!aLI0lW85XEj(GzA$_&v6>IBtU&mg3|(>{=j^;)PUG(yN76g-TP#*pXxK6i1m^aK9{IW++_QSD-BF91)=y zdFri)>#Iv9y_zBkC1+~Koa3!4QoU1Gjbk$@FqLbtZt@LB(i-c1=b5sej5LFgT@c7l zrcBZdIZ`EK8E`%2bbc9noYx^%sy-q)Qo#*11G>^mGL~!|Ghh!1_Ekr<6}D)%zql2Z zdxFZsJ;*?jvQ_TbSt7Nw!29&cCKn53)TXVo=29Bzs0u)UT+@K(n5jw0oxgv=T-M zhbMALWTz?U2(sBKKN2htP`PVp&~xTcg@K%aWe1a7xo55 z16InLBP)CoV(MU-z{oG*cZvs=Pvt01#TyJ0|1}p={T=cNKP(P465H;(I_mvAv*zN^ zXWYggK^k;;iIJm*`}sZrkq@`Fa^c^PLTs2AH~9a5RT$U$m-frYQJP?Ngcu>NFsQ!Y(Vb$TtAE*WyssjK( UY*^WTtg$h=3j@x@KZpbPHYadG{j4f-{Q5b_!p~#Hgkfkh#V{I^&D7#3GCNYk+D3g(; zkYpLVu?uC*TI8L2&--#*=e?in`RBRs@9%eA-|PGPJU8qT4J`)%0AK*DM++PGUuD6+ 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 diff --git a/tests/resources/ods/sheet_with_no_cells.ods b/tests/resources/ods/sheet_with_no_cells.ods deleted file mode 100644 index 0d30af9b44d9d2b612857e0dc22b6539f34229f4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6799 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 diff --git a/tests/resources/ods/sheet_with_number_rows_repeated.ods b/tests/resources/ods/sheet_with_number_rows_repeated.ods deleted file mode 100644 index 450148fd2e2d8286b61486dd8959955c5bd8b387..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2558 zcmZ{m2{hDeAIHbU3{jMj-9^flsc@r^WEtxiOmCK3GDDUb?u=z9%h(wVvXd-XCz372 zG}NnI5|e$#nx!mF#+Kzy-FshM=f2PX{LgdF|9QT@=bZ2PeLw#PX10g(5C8z+28`NR zL+!m;hiE_mAe09H;AMYx@y7U~Fuup}Xs;^7Ys?@-pk+##lkRQ=`&%_kz z-jbkV-c<0@lKDafW1k5>R!t9jkf&Ts6~sj;JHH~OM?BmqaNC3+K_QD-->h0Gz50d^ zalC%G#t4!uARwVEa>T45+>8T!`DWX)kZ^viy-tBJsn4!csA=zhke3MDF$qoMD-<)1 z2dXfPFU)I;aI}=y6Lb&QPT3FX)9JIN$QHo-J(c z#SXkwylBpdh|jsvD4B_$@z1p_&<-^JJ^I~eVSm}lh~B*c3e((Y-6R=uE!u+UY|do^qX!qcU;O^~+-vX*6zM9;vN$1@J5D-^-#$# zU`aL=(P#s}&|=S4@yKCkW#`aP@{8F`$0Uj>*ptzFu?b*B%nun-?Li} z@TPZ6^hg)6Zbfk_Y1wvUR%$!7)3mQ7ZM(QOJ?GmeSr;YmC!SxMRu^v=tY$S|UO$^u zCeV@{g^l!F&P6FS=Rf{!>p}fG7woU@2LLd()=+lC|8KAZ0I)E$)mMdD8maxgjT>Nj zMcTN2KE`Q|y*$MZiqOy7(9RflR}}6?TpCi_Jf1@Mq8Ebqj$a>}(=<=5HQ+ttRW{FY z;chj^K{ExO^WgTW>Dm*T83w_%N4{*{ZtYk;7@k}{`h!O1j6-tQ$U$&7Wnk)IR1fXiMyvbE{-(u4`Po{{{0#?+j|Fxpm& z_uKRy1j;|F2QF?S zKxG55P8*>wMOtFB2R&Z~%Vy7E!jg{p2v)02gs&2F_u%aDUEMAX%AeO9l{wLNj3OQ=Q9rf0Ni5U)45>P-dAeOk zu;L0aAjj@KU9{$XuT+9pmjUd`8THpj_@G~|cCr!{8B3D*bgD+fLtR*mvq?;Fq)=df zU0(?;DZyK-?E*yEnzlF1K=dw|er7Nxyeh3s6>A*WVq1i2RT>}D7>z-yS;~U*_5Jaq z5~tPr)aE;$QJF*($W5GS{BZ5{>@J_8ytLv~*!~=6zdw9(XFC<>gW_r<@Bn1EIa?FG z6uM#};ZV=5`O~;;ABR2^H^##dGc(NUmg84);2#GZ>1wYSkqvILOm0}jF!*R`F?{~kX>=V zjQ+!+M?^rb>PPuew^U~y6giqUG3|I+$WZU_{q9m_F+^CHB0=?))209Z$DmU~G@(+x zVF(d#T9fXQKCd5@Lvv8^cj%y8w;?ZZkq{GAZ?iLW{5E7{Eaa-B47~Q-6HV@j0 zietVxWJO%Oc6fOBN`l4x;*nX6`Gp(03vVyG_NQA}S+##Y;tj`WylN`pV}i13zQyp} zcI$*Lp|*l&ixe1qWkAyN3SN#>qFIed)}y{xlJV;40l9GL8dP0aOkq zeFT&^j(4wrqzzjh5Y;IM*~zO*iMf^6>S#34lQ-1cPbp!Q@G+UZl6e`&P78y~l?#Qd zJyhcKbl&pE#U_Fed>v5PCd#Fwq`$>}lsE#VW#F{;1D~~TT0i8ZAbJO+Ofh(uj&gnK zDv}bX60sgGCRey-nkhBZy&tQu7UV-lt<<(S`5w%}IJ;+x6F(}-JErg*9q|Z*SbM|T zHM(N-TJt*kQp7ecRR@U|h3CN*UAL|%ZWvMIqUGKxOmBV|^q8w|A=DrfA{!lYfP~fH z*oqIIRr3``<8}HofBzU+w+;Ig(=P!(C%)Y6GwAX-r{1=21^?xy_erL6k$H2+fu0Bq}Tu_N*m+jq|UH@g2V zYa|@m^ONjn*6*DB=MQ2x0C!3la_-=8w`w;Ic2sCCw(2i9>{jiL{EmvsR_y=)X2u2j Qz6N4H7uj%5`A!_bAJ0%k{r~^~ diff --git a/tests/resources/ods/sheet_with_only_one_cell.ods b/tests/resources/ods/sheet_with_only_one_cell.ods deleted file mode 100644 index c678ff67ef57dfe25ec4678e6b9309876850c33b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2531 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 diff --git a/tests/resources/ods/sheet_with_untrimmed_strings.ods b/tests/resources/ods/sheet_with_untrimmed_strings.ods deleted file mode 100644 index eb55bf87a0987dc823124d9f465f671a6a3b0847..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2454 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 SV|X_|>zh%Hd6V)00R9i6Adm9^ diff --git a/tests/resources/ods/two_sheets_with_custom_names.ods b/tests/resources/ods/two_sheets_with_custom_names.ods deleted file mode 100644 index 933a94c1ed5d31679dc607075ce18091faaf8905..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7200 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 C4!_R; diff --git a/tests/resources/ods/two_sheets_with_no_settings_xml_file.ods b/tests/resources/ods/two_sheets_with_no_settings_xml_file.ods deleted file mode 100644 index c2a78e71f0d24f107e074fef5b173de6b0202fd7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2633 zcmZ{m2UJtZ9*09WfNK^|7pVd&JweJ+L?l4yC3FN7gjf?w5)owSolqkPCg zh=3*15h6_>)JTs=uex#Hz6ZuJlt(Fub6&)#Gzuu(t

@3)hH6kO^rh72yCQInNlY(hsu*>D`RHf$B1I_H z?Hx1&KbaXEe5?_VO~y~RW{i4y$$UyzUrsoa3st^^Tkdygm+0sI<%CI(utHY^LOi^T zWC?QVKQDULaR4ytTd4FYSyW?tOOX35m$LGP3!8jOXUwNdv#q_`>ErZAV_2GrcKj<)F5{)0nRd54WOoO;jg z4lQ0*{aJ?mV~ns~w*TDfC{A8Ix-{^7k7xY|cWL@~#^qYd)36)EMxCp+{d?ZpK-+uSb%Shca`@x+&bRc%gyI6yjg z%Hk_>ypQ(hTMUo^{$zng-pRbdvg*xk2R+!r2Xq@sjkTT5Otj&I;EKyXc=`X!r!Z*EhB<+c7sN_Mp;AI2N8|%WX+}BEpFvxl&7-e7f z5u$3M^Ga6>H_V+FSuHS@c5Wd>?-9NBbb|x(0t+Uo;^SK>ew~XW1$K+_WcPfE@yo)T zqsOu((ys$Zd22p%abP~9anJ44r;2d%-j_2qEplY%`&MxjtYPSGS*%mb7J-j4AQhNi z?u%U4R2uBaeL5$rPU^_@oGVMd@usD~&Q^Z69vzf_9qS{L*5N_x4notq2Z&DUq*UXe z)fP>0Cx3R~td-8TqKdR26=FS$#otxKsGE8beMX`DuK^ivrh3-ea)xFw@PCIL0+_jo z=vULDoEI3~Q_KLs(fz|vt{yk8A~1iISq;AN=35ANJts;;e@Yq8pNcn$#ZozTdS27!qr!p z&>$XH!Ws9v1r<})C4KiNeK={+@y=_64p61Jj~AbWT~xCnRAOH4Pk^Gn;!vj;;1BK3EPO-3XWbTo#AO zU`9Ld1T`Oj6`e77t2sa{W5FXhLDGx296TAifqTf}d(kwIDkMxON~?RJ@}p!VT_otK zKgU1$%qp97Gcy4ICm4JVG5DYmSl6HCiBHz{2!nw1*3}|tD7I%Y(h zf%(avW^0Oh2m#ei*+FLcWuB)+0-Ldg6gTsQBS{erH&+@B-jH`!ok5o2LOkVJ=x>!I z&pRXZdy5mxqIy18$Y`Rp^*kL;=Ic?mLq!Dc?mP<8&bY1HqkH|mG}iT)rq;0LY~ff+ zYE5CCdkd}Cnf2)a(u{`=XFru9ckANgJdAUUcb~u$O5uCu_a>DJA@ARpHz@QbVIuD4 z8JWt~7Kq^oy1Ojzm_$6F-IB#Rn$`JR<<*9A$SlF8tXmUQ13|K;;zVIxeq??XrBe(!Iqd3EXPzy@zk@%n`DX4Bv_ zGVq=g%}K2n8v3Ai$U!yCO@^1X9`4$xNK~8cikf_~Gta48grepA5ppa((CzR{gfVHA zZg+vo-=E_xC?5|?~bV*8TTg%Zh^{rXB%qK3J>nG@b1Wcsj3s_{qT+j0w!Cu4u_ zjXSY|Gw*%abha2=tniD{<>~G)jqWO^BpiMC%0QN^$MG!IBFFSW8adRD8)zrN#_qH# z3z_9Eeh%GQd)ms=dF&6dzKix1^`o6aBtg{#Vcfs$a zR2YY)eY)S9@85*K7jxbp+pqtg zh=3*15h6_>)JTs=uex#Hz6ZuJlt(Fub6&)#Gzuu(t

@3)hH6kO^rh72yCQInNlY(hsu*>D`RHf$B1I_H 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 JH$Trn{{e0Az!d-h diff --git a/tests/resources/xlsx/attack_quadratic_blowup.xlsx b/tests/resources/xlsx/attack_quadratic_blowup.xlsx deleted file mode 100644 index 7c67c955e66fa3bd05d84103bb54effe29b2fa18..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3877 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 XHid@M|IB#^2m(R^!L3;P^9A}36all; diff --git a/tests/resources/xlsx/file_corrupted.xlsx b/tests/resources/xlsx/file_corrupted.xlsx deleted file mode 100644 index 552edd3..0000000 --- a/tests/resources/xlsx/file_corrupted.xlsx +++ /dev/null @@ -1,3 +0,0 @@ -csv--11,csv--12,csv--13 -csv--21,csv--22,csv--23 -csv--31,csv--32,csv--33 \ No newline at end of file diff --git a/tests/resources/xlsx/file_with_no_sheets_in_workbook_xml.xlsx b/tests/resources/xlsx/file_with_no_sheets_in_workbook_xml.xlsx deleted file mode 100644 index 74de5277f2c8b64188bc2dbc8d2b67a5365cfbfc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3704 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 TebYPy1Ob@?hYMYOp8@>`O9OBp diff --git a/tests/resources/xlsx/file_with_sheet_xml_not_matching_content_types.xlsx b/tests/resources/xlsx/file_with_sheet_xml_not_matching_content_types.xlsx deleted file mode 100644 index f33c9d7817f057af2fa1d0efe2b717be4c7e2953..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3757 zcmai12{@Gf79ZKBFxDb5woul=$X3>|FXKarB7=#s%*ei%iSfxUMOpfk)Fs={ib~^C z3SYL7?Af#jQK%Bt8=X7vbUo_VDf>n?g+Q&{N(9@$TRm+$J<9a)d@68#M@I1jP+>B+cz6Z&4 ziK(05C5!b}DL=(c8+RiRR6Ln4ywcF@KWo4H(DzgU?%>GH5N zLMBl2U2kBge`4s|v+$X4JFmeDGy6?gG>>-3wm8EeUnf4h!R%l8^Bq4Xc&sIDrXutk z>l2kw0;kcud(-g=1{?FP$C?lPM~$Orjzl>?MP`MRpVzj3edYNK9ig7&JlrH!GL%bm zow0~6?ocg{Ya-0P-%H+tB91-2qVZvp-~1=ZAW&-Af*!!@)?omCUGPSG_{jc}*-S)$ zjzH-M@Xm1`2*k7{BeNk{m||*N17jmh2HvX0)8s8a_Hwo#;KLc`WbTdg z+D#kRALQOD;I%4H!))8dP+nde#haB>eQlo{8)BO2yUsg1g|Au&c@cb|y%#yeAQ%oC zgBVIu7N0zOI5Kt4ulip8B;~fAUxbASs$ir{Pf`;p^rf`z)f)CiYd(?Gr>E_@z>;qt za9wuA@|aFP`1JmL?ghyDAhgMXxIs~4jou{&iMrBv-f%c`6Y1uv3Jq}?dpjyZl9mD7 zvbt|v{E}b)x;E{y{>$J|oK0YgbzMjHr}!|FkBa2VzwcDk1;zE0N;ORh37?%?PH7yr zdr?8VvqI#gz%txGm{Y*RzCBbYoHu$SR?lQRBuS8qFm1ZZ%Cac^ky4ucq~hbG5hthn zuyllzzEmoxsPk1Cxll&+t63w7)L%K|R$SrC*jUhG_2)%8Q3r(V4UFVG`B4kKQD~pu zY>|?A4sBq0esQr8>eeWK<+C0<(}7S9(^>_TWI0QA8JQ5pkTsu^GU zOKt9{iqDJPKiUvWkg<8`@bp7!mx8v@s=l}PTROhU*7WoI|C*B@zPe6tb$N@$y>&$k zuo&p^e+mx}_}zV#VDq;p19&6gzVdGB*%S-FdAmE}aPAu!`FVXC+(S7YkC{SYM-ZPhw;U?T>qH zMWR*OeBXm|Bw`eZwu%g{ZIwe)SD4+ZOanipe|l0yLi-Ac5LW+zH-y~14=Ic2tz?6Y z#)|cDTw|?^?y??b4V?C~eOYAPZ8;{{OgQDJKF(*P@y6SV@xJYy!kSRVowfO|g*UbF z01GdGXJfbkr1`j_ywT23%ZIkVZ@G)zm_w1ZPIO{-YSUG`T5HVHKR~#Fs@cMllZwCG{?n7EY zqp;zl`xgOZl9f~q9sd56+~3_uY(fbgG;R!YdKKPhu%U5)H2YR2uhCm{EqKMfn{W7eNI$TN2jdR9Y%5OU6z@`x#$ zTy$qn#iUq(l+z1NaAVG0%#*b1ou@25=UJdEaJ8?UU!&Q{G3&)t(p!LAMSy44O^&x; zmH0$0Gm<8=?xH~#YVcH6E>q;)+-@rayQh2(@01%J9#$hpKk0&|S$z5uzvel%yzU=X zcU>*V)p7hUd#k>r;Di_mf92GLJNB!1cRl+ZdrSV@et!m?E#=PE09 zx_z)dqs;Bq`e-7hQ~5xL<7APKXH!ww5or}=ObQDzUU5n7?{!Dp%g9Q~zF#r#Z8i{U z%s#GYwD0aoX0UQ1zz^W25DWqd?~(lhF~s_#JTT5?D9<1L{sy_*>?(}w^-Kt7Z^V4~ z#8SGPG3u~q7|j9=FPTNSJh#xSw!L)#JD9yiB z%wFZJ(nB_6cQU03^Y{*yH~4np8KkdymJl~;07f{)_d<`UGZgYUlIe?Yv36*9um3&q znf{c(5)6M1o^r%Q#|3G4UR>0gG^XtXoo-$tipGSn88zQ}sX$RwwkU11maz6;TYX#>K4ktJ$*MsRq9Q1^FRNlgPmA>mmd*ta5Mw6=f*-Tgz_^Q3q*};0R z;}9_UiZUJb79oA#cm0rE$XmS~Iq75Of~s6zjC7VHW_=jnMLIk?OoTm7>g!}l_E?FY z?^)7rh2HU_uSXZsGMd%};>WqojcI@;@!uC{AamQ!Z`Nt5g1t4eu^dtl8mNX}qi3hl z-OC~s6SIZ+F?;@(v5l>YdTs$xZQHSS+S|QlQDLvQ!+!2uRHK`#?e4{eiY@>q@RreE zYYY{>m!OS_O+7QfN6U}-{ma|EfSo~d(EkAZp48nFooYvAJ8XN?+Qso^xVvW|RgXT9 z-``C}YQ&owc8?>f1_s7W4O_!$H*|NGr9$0+z5*EE?miW~m$;2~MLqTak3Yd%&FdHG jn?CPuUsUY1J=othGjn75AJq>5K|p7LG;3^p1cCkoa=d>q diff --git a/tests/resources/xlsx/one_sheet_with_inline_strings.xlsx b/tests/resources/xlsx/one_sheet_with_inline_strings.xlsx deleted file mode 100644 index a839e825c9804a3b3697c2887fd89e0f4a0e15f7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3737 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 Y|AqPKD$@^}5D)}p2IPYhwW)yq3-97@xc~qF diff --git a/tests/resources/xlsx/one_sheet_with_invalid_xml_characters.xlsx b/tests/resources/xlsx/one_sheet_with_invalid_xml_characters.xlsx deleted file mode 100644 index 49293f5d3099312798ee4a81d91d3668cbf1cb01..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3597 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 O1P18WXTeS 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 diff --git a/tests/resources/xlsx/one_sheet_with_shared_strings_containing_text_and_hyperlink_in_same_cell.xlsx b/tests/resources/xlsx/one_sheet_with_shared_strings_containing_text_and_hyperlink_in_same_cell.xlsx deleted file mode 100644 index 8de03588cb6fed0fad548b96c18e575dfa54d172..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5560 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 diff --git a/tests/resources/xlsx/one_sheet_with_shared_strings_missing_unique_count_and_count.xlsx b/tests/resources/xlsx/one_sheet_with_shared_strings_missing_unique_count_and_count.xlsx deleted file mode 100644 index 6ef7ffde055059eeb48c1cb3d102fa471964f5a0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3609 zcmai12{@E%8y;lFIwV`NBw5Rr!HAPJ#4wH}QIuscVr+x4hGRLh%aIc)ODakXp>U*; zYzbMjS9Wsj8RY+l7}5X#?{$6GHP?L4{XXw=zt3|&&u5@Zb%+%N0?~lg| z9|TWG@0ouZAUT<`@#3ttejTcUUu4vD@!H-A{ed+sFIdDvJ001~j4)}dwM{D(Nz?X^ z3WFtZDs_XnUavU+g|fi8KnvF|SLbLEj4CI4BtO_nLBCFYbd<8J^5Pa1IWF?y_FTFD zGE<3yfA|rd#p|u2Q(!ZLzUL|ry~cGT=8lD3;}w|ahrg)p`dZ~&h6+9xZ#&v5R4|fF zaSM`vD4M;N(;-gEa@h5ZAyF73x~AMX&13L`WDv+WaY`HDb$5Y*Usv5xP9BmwHrp5B zt^DjOuFyrHe$-}G?5WH6! zbi&mzRw9CT(Rbge@OrQvp?)!O!tXSG#8Mb0u$&z?MbucZ2h*6;e-y}j>Kt6UEHRJc z`%L4wk~S`XzHllnqtSs^DQsSE@OEf?f}fap)2%??yu3qnz89uXoY^pGkkIxKb11wo zM`9T3jR~4ZP#3Trug~<>Me{v;ZHrE;a`4 zF0T7&WBt8zuo8HE7RX`dy=GRfuB7C>8()2sQkq#~mj1hXorTO-RgE(OAvjCdPCsp5 znA8OH!YSfPN!g>ZnaiHlS-I21JDQ%sh5}Z3V?~;$RFM3i3OlMcFfZG4@q{5wHHRg} zTubj;l50jH>ccc9w3OHLkWD@)#FDUfeshhc8(6Hqu+AL@V`wELuPadCS21_Of=^L_ zX+Eq|P71qu4sWVat!n+_JMLnJOEj(TN$-vcK(xqqR=l}aUhfkAyl~#I#}?7!*-6&Ff0XRfqZF0G-mg9sht`~v%rk+Tm*H$M>8%uL z%N)axi{c^A*c~~;okWBrnvrvBA3irOb{PNdW=d($XTXB=@eQJdC^#p@c3wBJyL~eU2_vZ&Kz~QS6X-Fs-Nn z5^+xdC+`4*UzMvEvzKKjfY$-am1}#SZ8L8d_v_X!F4svH@}qj+ITEB8f@Z@i|7Zb<^#7LrAraFHMa!Z243S+)(EY9Rf6Xi8~)jg#Xr;&V`OP~C>+l5(Hq&XkmWMKt! zq@Kfq2f8HV-WQ}OBPLC`ost!&*J^ZlspW<|^kts*_;8DJNL$70A436c{o_Pqil_^?r4m?2Z@Y;=xVyxQa~k!Y%(toJ;? zOZn$&sVK3_*7Q+zUafcDQfG~wb=D43GK#g-X(}2O&$@?GO)8trN9&&@su;u?Ckzio z!!db|4l(TZSoHU9&j`l;=C8NeY}~GrBO&#}OFFq@3P6F40QbuHBND8S6KX32PeVNy z!f}R5s=r>1-KOeAj(hsn#GVxn? zg#6_;taQyfiT7QwRdr<~4cZ((FbXy*p5>6Yruvg3;$w;y^oT6mTp|r+a-_9Q#f(3f zK#rCE`DZH-X48kbF}1tj$8NL+r!{aH!2DcW^c3GGE)?}OA68J*?J}2xK6p_YFgC=X zHO0ocki~mRSuEV|vN`byeR^m9y#)otGd@DbAWf_zL-tC}DT~2H;>ruoCE^lm*T$8N z-5T6(bxd|igtq{WeqeI>PbXuI`s~;%Dw#KVf=gkNN{S{z4Aa? zX1Ji%`+yR;UOp>!BPFp3qt{_@j=s`$!-fK|Eu<&tQG|Q0J-#}D*CYzuA?VoMPuvdFwKPDZr)9otTKQ55bc|haa zb-FV=kl_ajA~j0#`WN_A*lL?Qr#%SxImpA*TYz7ay1$W<{lNFa_6o#4j(^>P$opsl z_xr1cM2>iyu>I0c-hmmiy~A!J*bm*WXk;jsW(&Ia_+;=w;z-g!UJd|{z{yGAU7^?^ hod)zPsUTyM4q(Z`VW3OBmGe*#6r>Ma?sGa4?myU>J@^0s diff --git a/tests/resources/xlsx/sheet_with_all_cell_types.xlsx b/tests/resources/xlsx/sheet_with_all_cell_types.xlsx deleted file mode 100644 index ec109dead2a74a7381ee454a72473355fc65212f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3799 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 Of*gQDKTZ8y0{sUrnSi+f diff --git a/tests/resources/xlsx/sheet_with_custom_date_formats_and_no_apply_number_format.xlsx b/tests/resources/xlsx/sheet_with_custom_date_formats_and_no_apply_number_format.xlsx deleted file mode 100644 index 8718e71650c0ec5df03ba0e71dab7dc2050f3fd6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21882 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 Tx8p{F22FONw?w+uSKa>rNuJAp diff --git a/tests/resources/xlsx/sheet_with_dates_and_times.xlsx b/tests/resources/xlsx/sheet_with_dates_and_times.xlsx deleted file mode 100644 index b73d65ad8bf681fb58e8e4c560d14e4a1ebe31b2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22509 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 diff --git a/tests/resources/xlsx/sheet_with_different_numeric_value_times.xlsx b/tests/resources/xlsx/sheet_with_different_numeric_value_times.xlsx deleted file mode 100644 index e3ab29ee877adb8fe0cb91d6b8b2e3decfe18257..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28226 zcmeFZcU;p+_b>i|Ac&Mulny~bQE4hoF(fKV5h;R#fRII`Nr_Se1cK5*L`7jm1r-n# z5DP^Sh;$K=4gx|*P$_~W(mp~+eiOGm`|R$$`#k&I`}^nK_%cjp&gaaWGv}On&zVWA z%n&?lAzo-T1VQT|@ng&|2QCOg^FWX|w3^$|5bqc0<`;-N5_;M#z)6GP>!WywhkNra zh#S!Vzt{i88Yr}B_N(0{Qe`)Z-Cu2+csu^VgVk~sa*o1ti1&oc&j@KvR`9aLA!<~o zB2UIK2CVK%zV=4f$isW}y&LNN&!ip;`h3iNV&06A#D7z2*|tVCHdx|d*{YLIC#G9Z zKAw+~zO9~Kn9_kgbK$6f(GOJgu?O86Qlb-j+V7K3ZIKyS%;`WB-$?nvRbjWV?#;?Q z>bCms#j*p#yVmb)$qE_Mzkiv3ru4yn<($fbug=Gz6vWHYtHxixVm(%^7vEL(dUS}r zhN6(Ew92ib*ZS=7i1r}fjq9%`i%??M&k%FIr3$oYj-@x8_BIf9aw=GpTVNaKSF{JU z$Ha&0PR9WU0i`QL;R@}R@t5-5h;B+wq(nF-)kK;rt2)EcL|cVtyPwX;)fYpTgtv5s zw?@yJcp*<$FJaQ<^%wW^tlzqm=jqHPWRmvQF#&}1Z4GZ zE*bIWNR|+A$syoz0>CA4{%$@2ni`z<|L2PT7yI9DL@&H-Zc(>QBzDo{eNAS>Xo+Hs z)lEMU>yXv4h(ps}C&bvVg4b6W#2qlq6+3z^&tP56f&wS5PY}lc1hkZ&@)e7EizGdEwJ+0to#_LK&_aE_J{e7v26C8R-uhYe!OTV=}9%*Bw*^H zS=iWaC3fou@ogXDMb4}4mu-k4XWKfA14`yfpq;ia~Nh`oZ1Gv{WuefZ*D zel8)fOj0!L>Jn7vvw$4BDZR-nh%)@>M5)zWr`d?vs|*yb_j;3(WXeEr_VKXOLD_NYDP2i7##{I&dkU5T${_g_wZ%FSReUhv&b2)n@AcKD)z3f__F1E?*I5LT!i{W-2{um{OKE@8=A$aO|I}w~fsqO= zMv-{o*A$Vxs{6VEO|{c_EmwOlVx`I+Zj=mf*{y!)$2~W!)b=b_Un*+)csKnZ+Ty&3 z_h8#0LrQe5$##_RRlz%3PYXYsZoo-C^u`8S@4fQS`r!60IQ!wVPU=~w7>9=Z!L{O7gWd2%pD*(cY@ zcq-$nha%Q~ea}M(xo&MDIQ6aN-Rg^Hu{sR68&2k%SoQsIOOxMJo9T&D=mtgou1=wGpJIAwTk}P2NzWryjMCj3<&!JR^@Q z4>B)qn*DfgQ_GaetdFr*dRKPe^Uu`m8t>+t^JT(Xemd9Ogy-j&@;&QgrAF-kH)FfZXWD+>=8)hppmH�B`6fPWC13I!e@c)3H6xjD2IJi8j= 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 IK!5%AKM{|qp#T5? diff --git a/tests/resources/xlsx/sheet_with_dimensions_and_empty_cells.xlsx b/tests/resources/xlsx/sheet_with_dimensions_and_empty_cells.xlsx deleted file mode 100644 index cc0dee874186426022c4cc00eb84322a8fbe1f58..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3678 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 diff --git a/tests/resources/xlsx/sheet_with_empty_rows_and_missing_row_index.xlsx b/tests/resources/xlsx/sheet_with_empty_rows_and_missing_row_index.xlsx deleted file mode 100644 index 2f5cf154495c5d431a0caf947d7ce5eedb2f2e76..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3685 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 diff --git a/tests/resources/xlsx/sheet_with_empty_shared_string.xlsx b/tests/resources/xlsx/sheet_with_empty_shared_string.xlsx deleted file mode 100644 index f44da69ed47f32d1998eefb55408bf047419a66c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3628 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 F{sZ(DGgDc`-`AU@yd>$g|0H! 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 o{Li@OHmRI3 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 AmH+?% diff --git a/tests/resources/xlsx/sheet_with_missing_cell_reference.xlsx b/tests/resources/xlsx/sheet_with_missing_cell_reference.xlsx deleted file mode 100644 index 4e86cda28225a41372dcc870ba307eafda3857e2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5015 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 diff --git a/tests/resources/xlsx/sheet_with_no_cells.xlsx b/tests/resources/xlsx/sheet_with_no_cells.xlsx deleted file mode 100644 index c41e892dcbc09359bb8cbd82f1c4ce3ec45ea1ab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3543 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 I0~@;k07zj=F#rGn diff --git a/tests/resources/xlsx/sheet_with_pronunciation.xlsx b/tests/resources/xlsx/sheet_with_pronunciation.xlsx deleted file mode 100644 index 9eb37176f2b3cd894e5a56c1e1b97b52ee6c6299..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3725 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 diff --git a/tests/resources/xlsx/sheet_with_same_numeric_value_date_formatted_differently.xlsx b/tests/resources/xlsx/sheet_with_same_numeric_value_date_formatted_differently.xlsx deleted file mode 100644 index 8f5a20ce05f86dfecbb3912900a35f70316ee8f5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32960 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 diff --git a/tests/resources/xlsx/sheet_with_zeros_in_row.xlsx b/tests/resources/xlsx/sheet_with_zeros_in_row.xlsx deleted file mode 100644 index 3812e7d761765bb16743014dab77a01cdcac3812..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4732 zcmaJ_2RNMD*4BF&g6Kqv-a@nxM2QlN&Ws^CQHC)%dPI$=gM?9|m*_RoTcY=t2+>=J z5+S19nRD*_`E&0%>wBL0W}ew=f8T!hde>U-25MmAP+<@d5MVfhTvaiy88Pa+In2h% zT}Tl9S&A4|#})yE@A^l^`Fhn+D_K`JM~tMZ`aew`xUi5&5vKF-_YEch73JZ_ss(%> zlu$&@QQH29OnVrq0xbsdMa7)a`sQSk9e-tLb~GAfA2L+uAh+r(p6GHqWJS1ORxt?A znCf&Gn@KM{&SQKVZaru18{!Nx$G)`QIEpAbnzx@njW!HF4tZ5k^i?I?YsCypMQU`( zC_7Hzm|I0iw24A{SV_+yi(aeINyzKOSBrpq3S7Am;g;T@Umca)>LK$s9F{J3V?Cto z!d zuC9WAI6I|}$U1fk1Mn7_TRS4&)KQHm1FS<6$9P7_TsCc~Hq*F`Sya~-&Q6O3#8U1t z@9yu-@9#K$Si%ya+XYp{_gjPK9wg(oUUXsCUA?w)64Yr3$98&p0qn0(RfHvT!VEVad( z{~OJ(&qpLgJXS9SGH#1*RjnOnEFT`R?c<{HQF)@xyo6f49p*pbLvbA+D=5qc4Nx>f z8`{B28@_u=MSA>F_r^N1?sn>Tu>>3Uk5vq>%ah)m5oS$7YN>lP9V$9sD%@*;E3 z#Xmi(H0`B~=TN3l&3_`8O4%&1zK^3krDHN~sK!0Y%#Spbz#W&;!^?;sd(4Q>;2mEh zHO>jTSKuBc01nO)-w!!lRHsu&&qU%m_O;k!OKOaZ&@9+%GIci#PqjzH^dWKc=94`E z6C{enSh>~795|O1WKk7H5+uEJJPoss4Urig^1J-xkEcDJPX5!O;ED=SCUZ8uMS7cJgAx(iKO z)K?K|E8Q85JJi>(sosADo&*dthfDJR7@pGoN^=op#JF{Op0_Oo08xCXy*!4!vioQv z#MKTA3li9sgD8h7cEXd-FF=zL=*@`aDH+y6ZAAV*1}i;kGyZTA`isb3P?)176zYiH zkiSSBuZapu19^7EgV?{$_&y2KGvwU+^c3}7Og?U96$>F+J7 zKeoi8w<5X}N$`rZaYEa>JqenF&cE#}(iskB&o9_>##XRUmNea%d3sY#2vq%D(nQd* zBkmp~w^v=OXu9`Z@Q#j9|AE?g8s;FiUMU#6#)S{q=-zQzVKq%kW}f4lDcnO!I0$2o z7ai4&wOrs6x`Th{e6lLLD#yk&!`@dOoT)4BML210%e?DD3?-Z`1qN)NaeP0hPf;!{ zpP%!?LL;NScj7aKS~urEB18TcGS@MQ*6Ved6$UgP^Wn@p3wWB&RxmceLU1-HyBuKo zx$)gdHe6U!-bSmPl8Hm)(MYh;P__irHiKrm0OW%&Y-mqi?Mf?5u3tv{x=!TuBAG0I zpt{GPJkxDP(UhULaIBo6RNK9D4S@88D)pyx^Gi>vmK zA(Z^|Wq%4DQU598W@%i%rb|tth+=iCK6^IzL7=kuP8PGbt?&A#N#O$)D-}NJ)<%}O zA0|}BqgY%yn+?Zv&t?p1s~sd?B!|oR+T&AKf8@6|>U!5Iodv7ObSj5v~BJt8ncYrkV5BK}^6eH8gdt@ubfsc|@R4rGbEeG^GS< z>F0Q(yo{tBduvbj9K>4ab0r+qgEgjl`OlTBV1@2Jh?o{-#mgS-#8lDJqX{*qRqm6- z$z~?eR->*1YIM{HgcDhvp!np1{ZGk_^slIKhkHBOxTATrlMK*RXg`pc_)>d6Q4K+^JP+eLvL->_ML{fb&pW zpCNw*hn(y|RN*82KI{AK$JLwbH=?3A?{J!`Nw-tq{mFkpMF)2CF#!T$}gu4j!RF=|U9xg*^h~IZv zTsN7Ho@GG;BEuF4#C$H}vQ2>zmy|W;4yREX#nA)Cl$(c2lJXC33O>&Y@GGNR)ar|P z+;p&1q4KLsV5~D;t(?J#>b*BO@fK=qQWvOrl?tzLbGVq2m(QX|Hb3T65niTtmzLRs zatm+GA1EDKIteipdYu?=s@AdFPdG#%K~+Cy^n?GEC8HbC1jF&=&9d5J62>kNXIAxq zo1QOhHfZxk-%0vxfn4?EY&ZtQ8hV`c2EXDw{W$-t+Thbf&hq>|g*O@FDh-8-K4VgL zZ>it{dn$YyH^3~_pcwWl5~a|wZQKzTOn2U*Fb|J{v8pb$3xAO+xCJzcGXpCl<1UXK zYn`(7ol>qayZn(y_lG7DYhzzOvT?+AOOhFbJ5)X9$@OFlR<1KFay2Muuk(1~S9Y_Q z&x^12Gak!_0axf6lPEZ|v^nKJm-9drnb+nknAi-DL{2(P^fRYK!R5zCBh4+R{eJ7r zax+Kttv&P7yT48zeq~wEViC*R_G7KJVOoxf{9LexW8MgLnJ@a>nda7SEvmkTXSUMe znErN3RW3=4PjV-z$cE9+M&)SV!hO8Lw^RDJ4K$>`#jB(y__S#G)GW6a%mLHe zBI~opnKw54N49#}mhehi?U#(u0i9b*)A$mV0@qOm`R}_K+N(6^Zf|1)cNhBeCW4Ob zW#ypu+R-C#7zR~JKtkr^=VAa3 zQV`c(HGIzeR6)zp0zyQqwK>%CL3b?(?nfM=pF~`Vam(}^vo~$mJRX_rmcf`}S6oAMnS(1+=U)Q)r z%*{P{w`WB!zJ01YtS2g0(7@BZq+qN}2K#x^!R@45#o7;|FLxpUHFsjRUS2#C#Z#Qt z_%)bBJG__B=(Wl`ny}nwFqk4RA}*EMJvzGqgWZoFw$tp>Bw_g(214EjF-06n^2}?EN&KM`k|! zTCz*?>|O7EEiGl5n#!m@YI*r9Trc)#VS`@m=GSh`K$zQjLRZYaV@Pawpto3eXbvC9 zL2#$|fbtR1gkTy|@YD`o`n|d=fLEU$_h4pHjXpncS|K)rhK#GfdOW&9=exH4t|T6f z8EH=Si~biTeDR%>%3b93CVr!rtlz;l@mtfLL_+P@@@2q};T|*Eh<6Xti{+s|MO(go ziEoI%y=KbrHF390uzp0wO3tIDhH^yEcabn67~;UffFJ3{EngOg*@TH?$E=|fOd2!P zizzl{Fur%8@I_2oe6NXG%(v;~wnn?N+%my9Tul5<;&3SMobX+(6V5++R^669K;9Ke znmHR;O=q+tiXt05wHY-@<2AHdEM$f&L&tp1ZsiR%S#HOk?@^5@iA<4n9+C=_coAQo z4o-tUdXYX9;Qs#blGsB zw6&bO@a1w$VtW7vHCYp+9Jlla=OW4dUpZs+`WU@%(1|2TJ9_^vpGK%Z{LFB`F5$^r zQhB;#>0)IDtA+#woVYo=1K-uJ>R_g%2kc}G^eLr}o3`<0ul1rZ9k4n8RBt_ZvnRDC z4&B%J0VF<&9Es;2-6LE#;wuS~o%el7C0sG~v= zuZ#}$sNZGv@7~un4_anknG~wf{h#-LMCb4B*M$LE7+u+W6lwlmCjCCo^;QurBd%-# zg%xU^>!RX!=j&w|-A!H@4gR0bSH0!$uGi^}uG?3(jw1ZOpZi}`{P$U|6A7IPuIv-) peNeNw|DG6rpW%9Bpb2+nb!7jOen1UeJoF$!)Fp@7DXu@a{$KCi)`S26 diff --git a/tests/resources/xlsx/sheet_without_dimensions_and_empty_cells.xlsx b/tests/resources/xlsx/sheet_without_dimensions_and_empty_cells.xlsx deleted file mode 100644 index 9ab033d7fd350895fe5152f0433fe85c37a646b4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3662 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 CzC_#r diff --git a/tests/resources/xlsx/two_sheets_with_custom_names_and_custom_active_tab.xlsx b/tests/resources/xlsx/two_sheets_with_custom_names_and_custom_active_tab.xlsx deleted file mode 100644 index 8ba5f0234f4a1acfa51d64a91828eda2c2471214..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4331 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 diff --git a/tests/resources/xlsx/two_sheets_with_inline_strings.xlsx b/tests/resources/xlsx/two_sheets_with_inline_strings.xlsx deleted file mode 100644 index 67d6db356146a5f28acf7417270f017a1e3adb88..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4253 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 diff --git a/tests/resources/xlsx/two_sheets_with_shared_strings.xlsx b/tests/resources/xlsx/two_sheets_with_shared_strings.xlsx deleted file mode 100644 index 487e4866bee6f03b928e0af80208975104ef6cd7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4340 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 KBD35}xW54^Hc^ED