Xml.php 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880
  1. <?php
  2. namespace PhpOffice\PhpSpreadsheet\Reader;
  3. use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
  4. use PhpOffice\PhpSpreadsheet\Cell\DataType;
  5. use PhpOffice\PhpSpreadsheet\Document\Properties;
  6. use PhpOffice\PhpSpreadsheet\RichText\RichText;
  7. use PhpOffice\PhpSpreadsheet\Settings;
  8. use PhpOffice\PhpSpreadsheet\Shared\Date;
  9. use PhpOffice\PhpSpreadsheet\Shared\File;
  10. use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
  11. use PhpOffice\PhpSpreadsheet\Spreadsheet;
  12. use PhpOffice\PhpSpreadsheet\Style\Alignment;
  13. use PhpOffice\PhpSpreadsheet\Style\Border;
  14. use PhpOffice\PhpSpreadsheet\Style\Font;
  15. use SimpleXMLElement;
  16. /**
  17. * Reader for SpreadsheetML, the XML schema for Microsoft Office Excel 2003.
  18. */
  19. class Xml extends BaseReader
  20. {
  21. /**
  22. * Formats.
  23. *
  24. * @var array
  25. */
  26. protected $styles = [];
  27. /**
  28. * Character set used in the file.
  29. *
  30. * @var string
  31. */
  32. protected $charSet = 'UTF-8';
  33. /**
  34. * Create a new Excel2003XML Reader instance.
  35. */
  36. public function __construct()
  37. {
  38. $this->readFilter = new DefaultReadFilter();
  39. }
  40. /**
  41. * Can the current IReader read the file?
  42. *
  43. * @param string $pFilename
  44. *
  45. * @throws Exception
  46. *
  47. * @return bool
  48. */
  49. public function canRead($pFilename)
  50. {
  51. // Office xmlns:o="urn:schemas-microsoft-com:office:office"
  52. // Excel xmlns:x="urn:schemas-microsoft-com:office:excel"
  53. // XML Spreadsheet xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet"
  54. // Spreadsheet component xmlns:c="urn:schemas-microsoft-com:office:component:spreadsheet"
  55. // XML schema xmlns:s="uuid:BDC6E3F0-6DA3-11d1-A2A3-00AA00C14882"
  56. // XML data type xmlns:dt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882"
  57. // MS-persist recordset xmlns:rs="urn:schemas-microsoft-com:rowset"
  58. // Rowset xmlns:z="#RowsetSchema"
  59. //
  60. $signature = [
  61. '<?xml version="1.0"',
  62. '<?mso-application progid="Excel.Sheet"?>',
  63. ];
  64. // Open file
  65. $this->openFile($pFilename);
  66. $fileHandle = $this->fileHandle;
  67. // Read sample data (first 2 KB will do)
  68. $data = fread($fileHandle, 2048);
  69. fclose($fileHandle);
  70. $data = strtr($data, "'", '"'); // fix headers with single quote
  71. $valid = true;
  72. foreach ($signature as $match) {
  73. // every part of the signature must be present
  74. if (strpos($data, $match) === false) {
  75. $valid = false;
  76. break;
  77. }
  78. }
  79. // Retrieve charset encoding
  80. if (preg_match('/<?xml.*encoding=[\'"](.*?)[\'"].*?>/um', $data, $matches)) {
  81. $this->charSet = strtoupper($matches[1]);
  82. }
  83. return $valid;
  84. }
  85. /**
  86. * Check if the file is a valid SimpleXML.
  87. *
  88. * @param string $pFilename
  89. *
  90. * @throws Exception
  91. *
  92. * @return false|\SimpleXMLElement
  93. */
  94. public function trySimpleXMLLoadString($pFilename)
  95. {
  96. try {
  97. $xml = simplexml_load_string(
  98. $this->securityScan(file_get_contents($pFilename)),
  99. 'SimpleXMLElement',
  100. Settings::getLibXmlLoaderOptions()
  101. );
  102. } catch (\Exception $e) {
  103. throw new Exception('Cannot load invalid XML file: ' . $pFilename, 0, $e);
  104. }
  105. return $xml;
  106. }
  107. /**
  108. * Reads names of the worksheets from a file, without parsing the whole file to a Spreadsheet object.
  109. *
  110. * @param string $pFilename
  111. *
  112. * @throws Exception
  113. *
  114. * @return array
  115. */
  116. public function listWorksheetNames($pFilename)
  117. {
  118. File::assertFile($pFilename);
  119. if (!$this->canRead($pFilename)) {
  120. throw new Exception($pFilename . ' is an Invalid Spreadsheet file.');
  121. }
  122. $worksheetNames = [];
  123. $xml = $this->trySimpleXMLLoadString($pFilename);
  124. $namespaces = $xml->getNamespaces(true);
  125. $xml_ss = $xml->children($namespaces['ss']);
  126. foreach ($xml_ss->Worksheet as $worksheet) {
  127. $worksheet_ss = $worksheet->attributes($namespaces['ss']);
  128. $worksheetNames[] = self::convertStringEncoding((string) $worksheet_ss['Name'], $this->charSet);
  129. }
  130. return $worksheetNames;
  131. }
  132. /**
  133. * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
  134. *
  135. * @param string $pFilename
  136. *
  137. * @throws Exception
  138. *
  139. * @return array
  140. */
  141. public function listWorksheetInfo($pFilename)
  142. {
  143. File::assertFile($pFilename);
  144. $worksheetInfo = [];
  145. $xml = $this->trySimpleXMLLoadString($pFilename);
  146. $namespaces = $xml->getNamespaces(true);
  147. $worksheetID = 1;
  148. $xml_ss = $xml->children($namespaces['ss']);
  149. foreach ($xml_ss->Worksheet as $worksheet) {
  150. $worksheet_ss = $worksheet->attributes($namespaces['ss']);
  151. $tmpInfo = [];
  152. $tmpInfo['worksheetName'] = '';
  153. $tmpInfo['lastColumnLetter'] = 'A';
  154. $tmpInfo['lastColumnIndex'] = 0;
  155. $tmpInfo['totalRows'] = 0;
  156. $tmpInfo['totalColumns'] = 0;
  157. if (isset($worksheet_ss['Name'])) {
  158. $tmpInfo['worksheetName'] = (string) $worksheet_ss['Name'];
  159. } else {
  160. $tmpInfo['worksheetName'] = "Worksheet_{$worksheetID}";
  161. }
  162. if (isset($worksheet->Table->Row)) {
  163. $rowIndex = 0;
  164. foreach ($worksheet->Table->Row as $rowData) {
  165. $columnIndex = 0;
  166. $rowHasData = false;
  167. foreach ($rowData->Cell as $cell) {
  168. if (isset($cell->Data)) {
  169. $tmpInfo['lastColumnIndex'] = max($tmpInfo['lastColumnIndex'], $columnIndex);
  170. $rowHasData = true;
  171. }
  172. ++$columnIndex;
  173. }
  174. ++$rowIndex;
  175. if ($rowHasData) {
  176. $tmpInfo['totalRows'] = max($tmpInfo['totalRows'], $rowIndex);
  177. }
  178. }
  179. }
  180. $tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1);
  181. $tmpInfo['totalColumns'] = $tmpInfo['lastColumnIndex'] + 1;
  182. $worksheetInfo[] = $tmpInfo;
  183. ++$worksheetID;
  184. }
  185. return $worksheetInfo;
  186. }
  187. /**
  188. * Loads Spreadsheet from file.
  189. *
  190. * @param string $pFilename
  191. *
  192. * @throws Exception
  193. *
  194. * @return Spreadsheet
  195. */
  196. public function load($pFilename)
  197. {
  198. // Create new Spreadsheet
  199. $spreadsheet = new Spreadsheet();
  200. $spreadsheet->removeSheetByIndex(0);
  201. // Load into this instance
  202. return $this->loadIntoExisting($pFilename, $spreadsheet);
  203. }
  204. private static function identifyFixedStyleValue($styleList, &$styleAttributeValue)
  205. {
  206. $styleAttributeValue = strtolower($styleAttributeValue);
  207. foreach ($styleList as $style) {
  208. if ($styleAttributeValue == strtolower($style)) {
  209. $styleAttributeValue = $style;
  210. return true;
  211. }
  212. }
  213. return false;
  214. }
  215. /**
  216. * pixel units to excel width units(units of 1/256th of a character width).
  217. *
  218. * @param float $pxs
  219. *
  220. * @return float
  221. */
  222. protected static function pixel2WidthUnits($pxs)
  223. {
  224. $UNIT_OFFSET_MAP = [0, 36, 73, 109, 146, 182, 219];
  225. $widthUnits = 256 * ($pxs / 7);
  226. $widthUnits += $UNIT_OFFSET_MAP[($pxs % 7)];
  227. return $widthUnits;
  228. }
  229. /**
  230. * excel width units(units of 1/256th of a character width) to pixel units.
  231. *
  232. * @param float $widthUnits
  233. *
  234. * @return float
  235. */
  236. protected static function widthUnits2Pixel($widthUnits)
  237. {
  238. $pixels = ($widthUnits / 256) * 7;
  239. $offsetWidthUnits = $widthUnits % 256;
  240. $pixels += round($offsetWidthUnits / (256 / 7));
  241. return $pixels;
  242. }
  243. protected static function hex2str($hex)
  244. {
  245. return chr(hexdec($hex[1]));
  246. }
  247. /**
  248. * Loads from file into Spreadsheet instance.
  249. *
  250. * @param string $pFilename
  251. * @param Spreadsheet $spreadsheet
  252. *
  253. * @throws Exception
  254. *
  255. * @return Spreadsheet
  256. */
  257. public function loadIntoExisting($pFilename, Spreadsheet $spreadsheet)
  258. {
  259. File::assertFile($pFilename);
  260. if (!$this->canRead($pFilename)) {
  261. throw new Exception($pFilename . ' is an Invalid Spreadsheet file.');
  262. }
  263. $xml = $this->trySimpleXMLLoadString($pFilename);
  264. $namespaces = $xml->getNamespaces(true);
  265. $docProps = $spreadsheet->getProperties();
  266. if (isset($xml->DocumentProperties[0])) {
  267. foreach ($xml->DocumentProperties[0] as $propertyName => $propertyValue) {
  268. switch ($propertyName) {
  269. case 'Title':
  270. $docProps->setTitle(self::convertStringEncoding($propertyValue, $this->charSet));
  271. break;
  272. case 'Subject':
  273. $docProps->setSubject(self::convertStringEncoding($propertyValue, $this->charSet));
  274. break;
  275. case 'Author':
  276. $docProps->setCreator(self::convertStringEncoding($propertyValue, $this->charSet));
  277. break;
  278. case 'Created':
  279. $creationDate = strtotime($propertyValue);
  280. $docProps->setCreated($creationDate);
  281. break;
  282. case 'LastAuthor':
  283. $docProps->setLastModifiedBy(self::convertStringEncoding($propertyValue, $this->charSet));
  284. break;
  285. case 'LastSaved':
  286. $lastSaveDate = strtotime($propertyValue);
  287. $docProps->setModified($lastSaveDate);
  288. break;
  289. case 'Company':
  290. $docProps->setCompany(self::convertStringEncoding($propertyValue, $this->charSet));
  291. break;
  292. case 'Category':
  293. $docProps->setCategory(self::convertStringEncoding($propertyValue, $this->charSet));
  294. break;
  295. case 'Manager':
  296. $docProps->setManager(self::convertStringEncoding($propertyValue, $this->charSet));
  297. break;
  298. case 'Keywords':
  299. $docProps->setKeywords(self::convertStringEncoding($propertyValue, $this->charSet));
  300. break;
  301. case 'Description':
  302. $docProps->setDescription(self::convertStringEncoding($propertyValue, $this->charSet));
  303. break;
  304. }
  305. }
  306. }
  307. if (isset($xml->CustomDocumentProperties)) {
  308. foreach ($xml->CustomDocumentProperties[0] as $propertyName => $propertyValue) {
  309. $propertyAttributes = $propertyValue->attributes($namespaces['dt']);
  310. $propertyName = preg_replace_callback('/_x([0-9a-z]{4})_/', ['self', 'hex2str'], $propertyName);
  311. $propertyType = Properties::PROPERTY_TYPE_UNKNOWN;
  312. switch ((string) $propertyAttributes) {
  313. case 'string':
  314. $propertyType = Properties::PROPERTY_TYPE_STRING;
  315. $propertyValue = trim($propertyValue);
  316. break;
  317. case 'boolean':
  318. $propertyType = Properties::PROPERTY_TYPE_BOOLEAN;
  319. $propertyValue = (bool) $propertyValue;
  320. break;
  321. case 'integer':
  322. $propertyType = Properties::PROPERTY_TYPE_INTEGER;
  323. $propertyValue = (int) $propertyValue;
  324. break;
  325. case 'float':
  326. $propertyType = Properties::PROPERTY_TYPE_FLOAT;
  327. $propertyValue = (float) $propertyValue;
  328. break;
  329. case 'dateTime.tz':
  330. $propertyType = Properties::PROPERTY_TYPE_DATE;
  331. $propertyValue = strtotime(trim($propertyValue));
  332. break;
  333. }
  334. $docProps->setCustomProperty($propertyName, $propertyValue, $propertyType);
  335. }
  336. }
  337. $this->parseStyles($xml, $namespaces);
  338. $worksheetID = 0;
  339. $xml_ss = $xml->children($namespaces['ss']);
  340. foreach ($xml_ss->Worksheet as $worksheet) {
  341. $worksheet_ss = $worksheet->attributes($namespaces['ss']);
  342. if ((isset($this->loadSheetsOnly)) && (isset($worksheet_ss['Name'])) &&
  343. (!in_array($worksheet_ss['Name'], $this->loadSheetsOnly))) {
  344. continue;
  345. }
  346. // Create new Worksheet
  347. $spreadsheet->createSheet();
  348. $spreadsheet->setActiveSheetIndex($worksheetID);
  349. if (isset($worksheet_ss['Name'])) {
  350. $worksheetName = self::convertStringEncoding((string) $worksheet_ss['Name'], $this->charSet);
  351. // Use false for $updateFormulaCellReferences to prevent adjustment of worksheet references in
  352. // formula cells... during the load, all formulae should be correct, and we're simply bringing
  353. // the worksheet name in line with the formula, not the reverse
  354. $spreadsheet->getActiveSheet()->setTitle($worksheetName, false, false);
  355. }
  356. $columnID = 'A';
  357. if (isset($worksheet->Table->Column)) {
  358. foreach ($worksheet->Table->Column as $columnData) {
  359. $columnData_ss = $columnData->attributes($namespaces['ss']);
  360. if (isset($columnData_ss['Index'])) {
  361. $columnID = Coordinate::stringFromColumnIndex((int) $columnData_ss['Index']);
  362. }
  363. if (isset($columnData_ss['Width'])) {
  364. $columnWidth = $columnData_ss['Width'];
  365. $spreadsheet->getActiveSheet()->getColumnDimension($columnID)->setWidth($columnWidth / 5.4);
  366. }
  367. ++$columnID;
  368. }
  369. }
  370. $rowID = 1;
  371. if (isset($worksheet->Table->Row)) {
  372. $additionalMergedCells = 0;
  373. foreach ($worksheet->Table->Row as $rowData) {
  374. $rowHasData = false;
  375. $row_ss = $rowData->attributes($namespaces['ss']);
  376. if (isset($row_ss['Index'])) {
  377. $rowID = (int) $row_ss['Index'];
  378. }
  379. $columnID = 'A';
  380. foreach ($rowData->Cell as $cell) {
  381. $cell_ss = $cell->attributes($namespaces['ss']);
  382. if (isset($cell_ss['Index'])) {
  383. $columnID = Coordinate::stringFromColumnIndex((int) $cell_ss['Index']);
  384. }
  385. $cellRange = $columnID . $rowID;
  386. if ($this->getReadFilter() !== null) {
  387. if (!$this->getReadFilter()->readCell($columnID, $rowID, $worksheetName)) {
  388. ++$columnID;
  389. continue;
  390. }
  391. }
  392. if (isset($cell_ss['HRef'])) {
  393. $spreadsheet->getActiveSheet()->getCell($cellRange)->getHyperlink()->setUrl($cell_ss['HRef']);
  394. }
  395. if ((isset($cell_ss['MergeAcross'])) || (isset($cell_ss['MergeDown']))) {
  396. $columnTo = $columnID;
  397. if (isset($cell_ss['MergeAcross'])) {
  398. $additionalMergedCells += (int) $cell_ss['MergeAcross'];
  399. $columnTo = Coordinate::stringFromColumnIndex(Coordinate::columnIndexFromString($columnID) + $cell_ss['MergeAcross']);
  400. }
  401. $rowTo = $rowID;
  402. if (isset($cell_ss['MergeDown'])) {
  403. $rowTo = $rowTo + $cell_ss['MergeDown'];
  404. }
  405. $cellRange .= ':' . $columnTo . $rowTo;
  406. $spreadsheet->getActiveSheet()->mergeCells($cellRange);
  407. }
  408. $cellIsSet = $hasCalculatedValue = false;
  409. $cellDataFormula = '';
  410. if (isset($cell_ss['Formula'])) {
  411. $cellDataFormula = $cell_ss['Formula'];
  412. $hasCalculatedValue = true;
  413. }
  414. if (isset($cell->Data)) {
  415. $cellValue = $cellData = $cell->Data;
  416. $type = DataType::TYPE_NULL;
  417. $cellData_ss = $cellData->attributes($namespaces['ss']);
  418. if (isset($cellData_ss['Type'])) {
  419. $cellDataType = $cellData_ss['Type'];
  420. switch ($cellDataType) {
  421. /*
  422. const TYPE_STRING = 's';
  423. const TYPE_FORMULA = 'f';
  424. const TYPE_NUMERIC = 'n';
  425. const TYPE_BOOL = 'b';
  426. const TYPE_NULL = 'null';
  427. const TYPE_INLINE = 'inlineStr';
  428. const TYPE_ERROR = 'e';
  429. */
  430. case 'String':
  431. $cellValue = self::convertStringEncoding($cellValue, $this->charSet);
  432. $type = DataType::TYPE_STRING;
  433. break;
  434. case 'Number':
  435. $type = DataType::TYPE_NUMERIC;
  436. $cellValue = (float) $cellValue;
  437. if (floor($cellValue) == $cellValue) {
  438. $cellValue = (int) $cellValue;
  439. }
  440. break;
  441. case 'Boolean':
  442. $type = DataType::TYPE_BOOL;
  443. $cellValue = ($cellValue != 0);
  444. break;
  445. case 'DateTime':
  446. $type = DataType::TYPE_NUMERIC;
  447. $cellValue = Date::PHPToExcel(strtotime($cellValue));
  448. break;
  449. case 'Error':
  450. $type = DataType::TYPE_ERROR;
  451. break;
  452. }
  453. }
  454. if ($hasCalculatedValue) {
  455. $type = DataType::TYPE_FORMULA;
  456. $columnNumber = Coordinate::columnIndexFromString($columnID);
  457. if (substr($cellDataFormula, 0, 3) == 'of:') {
  458. $cellDataFormula = substr($cellDataFormula, 3);
  459. $temp = explode('"', $cellDataFormula);
  460. $key = false;
  461. foreach ($temp as &$value) {
  462. // Only replace in alternate array entries (i.e. non-quoted blocks)
  463. if ($key = !$key) {
  464. $value = str_replace(['[.', '.', ']'], '', $value);
  465. }
  466. }
  467. } else {
  468. // Convert R1C1 style references to A1 style references (but only when not quoted)
  469. $temp = explode('"', $cellDataFormula);
  470. $key = false;
  471. foreach ($temp as &$value) {
  472. // Only replace in alternate array entries (i.e. non-quoted blocks)
  473. if ($key = !$key) {
  474. preg_match_all('/(R(\[?-?\d*\]?))(C(\[?-?\d*\]?))/', $value, $cellReferences, PREG_SET_ORDER + PREG_OFFSET_CAPTURE);
  475. // Reverse the matches array, otherwise all our offsets will become incorrect if we modify our way
  476. // through the formula from left to right. Reversing means that we work right to left.through
  477. // the formula
  478. $cellReferences = array_reverse($cellReferences);
  479. // Loop through each R1C1 style reference in turn, converting it to its A1 style equivalent,
  480. // then modify the formula to use that new reference
  481. foreach ($cellReferences as $cellReference) {
  482. $rowReference = $cellReference[2][0];
  483. // Empty R reference is the current row
  484. if ($rowReference == '') {
  485. $rowReference = $rowID;
  486. }
  487. // Bracketed R references are relative to the current row
  488. if ($rowReference[0] == '[') {
  489. $rowReference = $rowID + trim($rowReference, '[]');
  490. }
  491. $columnReference = $cellReference[4][0];
  492. // Empty C reference is the current column
  493. if ($columnReference == '') {
  494. $columnReference = $columnNumber;
  495. }
  496. // Bracketed C references are relative to the current column
  497. if ($columnReference[0] == '[') {
  498. $columnReference = $columnNumber + trim($columnReference, '[]');
  499. }
  500. $A1CellReference = Coordinate::stringFromColumnIndex($columnReference) . $rowReference;
  501. $value = substr_replace($value, $A1CellReference, $cellReference[0][1], strlen($cellReference[0][0]));
  502. }
  503. }
  504. }
  505. }
  506. unset($value);
  507. // Then rebuild the formula string
  508. $cellDataFormula = implode('"', $temp);
  509. }
  510. $spreadsheet->getActiveSheet()->getCell($columnID . $rowID)->setValueExplicit((($hasCalculatedValue) ? $cellDataFormula : $cellValue), $type);
  511. if ($hasCalculatedValue) {
  512. $spreadsheet->getActiveSheet()->getCell($columnID . $rowID)->setCalculatedValue($cellValue);
  513. }
  514. $cellIsSet = $rowHasData = true;
  515. }
  516. if (isset($cell->Comment)) {
  517. $commentAttributes = $cell->Comment->attributes($namespaces['ss']);
  518. $author = 'unknown';
  519. if (isset($commentAttributes->Author)) {
  520. $author = (string) $commentAttributes->Author;
  521. }
  522. $node = $cell->Comment->Data->asXML();
  523. $annotation = strip_tags($node);
  524. $spreadsheet->getActiveSheet()->getComment($columnID . $rowID)->setAuthor(self::convertStringEncoding($author, $this->charSet))->setText($this->parseRichText($annotation));
  525. }
  526. if (($cellIsSet) && (isset($cell_ss['StyleID']))) {
  527. $style = (string) $cell_ss['StyleID'];
  528. if ((isset($this->styles[$style])) && (!empty($this->styles[$style]))) {
  529. if (!$spreadsheet->getActiveSheet()->cellExists($columnID . $rowID)) {
  530. $spreadsheet->getActiveSheet()->getCell($columnID . $rowID)->setValue(null);
  531. }
  532. $spreadsheet->getActiveSheet()->getStyle($cellRange)->applyFromArray($this->styles[$style]);
  533. }
  534. }
  535. ++$columnID;
  536. while ($additionalMergedCells > 0) {
  537. ++$columnID;
  538. --$additionalMergedCells;
  539. }
  540. }
  541. if ($rowHasData) {
  542. if (isset($row_ss['Height'])) {
  543. $rowHeight = $row_ss['Height'];
  544. $spreadsheet->getActiveSheet()->getRowDimension($rowID)->setRowHeight($rowHeight);
  545. }
  546. }
  547. ++$rowID;
  548. }
  549. }
  550. ++$worksheetID;
  551. }
  552. // Return
  553. return $spreadsheet;
  554. }
  555. protected static function convertStringEncoding($string, $charset)
  556. {
  557. if ($charset != 'UTF-8') {
  558. return StringHelper::convertEncoding($string, 'UTF-8', $charset);
  559. }
  560. return $string;
  561. }
  562. protected function parseRichText($is)
  563. {
  564. $value = new RichText();
  565. $value->createText(self::convertStringEncoding($is, $this->charSet));
  566. return $value;
  567. }
  568. /**
  569. * @param SimpleXMLElement $xml
  570. * @param array $namespaces
  571. */
  572. private function parseStyles(SimpleXMLElement $xml, array $namespaces)
  573. {
  574. if (!isset($xml->Styles)) {
  575. return;
  576. }
  577. foreach ($xml->Styles[0] as $style) {
  578. $style_ss = $style->attributes($namespaces['ss']);
  579. $styleID = (string) $style_ss['ID'];
  580. $this->styles[$styleID] = (isset($this->styles['Default'])) ? $this->styles['Default'] : [];
  581. foreach ($style as $styleType => $styleData) {
  582. $styleAttributes = $styleData->attributes($namespaces['ss']);
  583. switch ($styleType) {
  584. case 'Alignment':
  585. $this->parseStyleAlignment($styleID, $styleAttributes);
  586. break;
  587. case 'Borders':
  588. $this->parseStyleBorders($styleID, $styleData, $namespaces);
  589. break;
  590. case 'Font':
  591. $this->parseStyleFont($styleID, $styleAttributes);
  592. break;
  593. case 'Interior':
  594. $this->parseStyleInterior($styleID, $styleAttributes);
  595. break;
  596. case 'NumberFormat':
  597. $this->parseStyleNumberFormat($styleID, $styleAttributes);
  598. break;
  599. }
  600. }
  601. }
  602. }
  603. /**
  604. * @param string $styleID
  605. * @param SimpleXMLElement $styleAttributes
  606. */
  607. private function parseStyleAlignment($styleID, SimpleXMLElement $styleAttributes)
  608. {
  609. $verticalAlignmentStyles = [
  610. Alignment::VERTICAL_BOTTOM,
  611. Alignment::VERTICAL_TOP,
  612. Alignment::VERTICAL_CENTER,
  613. Alignment::VERTICAL_JUSTIFY,
  614. ];
  615. $horizontalAlignmentStyles = [
  616. Alignment::HORIZONTAL_GENERAL,
  617. Alignment::HORIZONTAL_LEFT,
  618. Alignment::HORIZONTAL_RIGHT,
  619. Alignment::HORIZONTAL_CENTER,
  620. Alignment::HORIZONTAL_CENTER_CONTINUOUS,
  621. Alignment::HORIZONTAL_JUSTIFY,
  622. ];
  623. foreach ($styleAttributes as $styleAttributeKey => $styleAttributeValue) {
  624. $styleAttributeValue = (string) $styleAttributeValue;
  625. switch ($styleAttributeKey) {
  626. case 'Vertical':
  627. if (self::identifyFixedStyleValue($verticalAlignmentStyles, $styleAttributeValue)) {
  628. $this->styles[$styleID]['alignment']['vertical'] = $styleAttributeValue;
  629. }
  630. break;
  631. case 'Horizontal':
  632. if (self::identifyFixedStyleValue($horizontalAlignmentStyles, $styleAttributeValue)) {
  633. $this->styles[$styleID]['alignment']['horizontal'] = $styleAttributeValue;
  634. }
  635. break;
  636. case 'WrapText':
  637. $this->styles[$styleID]['alignment']['wrapText'] = true;
  638. break;
  639. }
  640. }
  641. }
  642. /**
  643. * @param $styleID
  644. * @param SimpleXMLElement $styleData
  645. * @param array $namespaces
  646. */
  647. private function parseStyleBorders($styleID, SimpleXMLElement $styleData, array $namespaces)
  648. {
  649. foreach ($styleData->Border as $borderStyle) {
  650. $borderAttributes = $borderStyle->attributes($namespaces['ss']);
  651. $thisBorder = [];
  652. foreach ($borderAttributes as $borderStyleKey => $borderStyleValue) {
  653. switch ($borderStyleKey) {
  654. case 'LineStyle':
  655. $thisBorder['borderStyle'] = Border::BORDER_MEDIUM;
  656. break;
  657. case 'Weight':
  658. break;
  659. case 'Position':
  660. $borderPosition = strtolower($borderStyleValue);
  661. break;
  662. case 'Color':
  663. $borderColour = substr($borderStyleValue, 1);
  664. $thisBorder['color']['rgb'] = $borderColour;
  665. break;
  666. }
  667. }
  668. if (!empty($thisBorder)) {
  669. if (($borderPosition == 'left') || ($borderPosition == 'right') || ($borderPosition == 'top') || ($borderPosition == 'bottom')) {
  670. $this->styles[$styleID]['borders'][$borderPosition] = $thisBorder;
  671. }
  672. }
  673. }
  674. }
  675. /**
  676. * @param $styleID
  677. * @param SimpleXMLElement $styleAttributes
  678. */
  679. private function parseStyleFont($styleID, SimpleXMLElement $styleAttributes)
  680. {
  681. $underlineStyles = [
  682. Font::UNDERLINE_NONE,
  683. Font::UNDERLINE_DOUBLE,
  684. Font::UNDERLINE_DOUBLEACCOUNTING,
  685. Font::UNDERLINE_SINGLE,
  686. Font::UNDERLINE_SINGLEACCOUNTING,
  687. ];
  688. foreach ($styleAttributes as $styleAttributeKey => $styleAttributeValue) {
  689. $styleAttributeValue = (string) $styleAttributeValue;
  690. switch ($styleAttributeKey) {
  691. case 'FontName':
  692. $this->styles[$styleID]['font']['name'] = $styleAttributeValue;
  693. break;
  694. case 'Size':
  695. $this->styles[$styleID]['font']['size'] = $styleAttributeValue;
  696. break;
  697. case 'Color':
  698. $this->styles[$styleID]['font']['color']['rgb'] = substr($styleAttributeValue, 1);
  699. break;
  700. case 'Bold':
  701. $this->styles[$styleID]['font']['bold'] = true;
  702. break;
  703. case 'Italic':
  704. $this->styles[$styleID]['font']['italic'] = true;
  705. break;
  706. case 'Underline':
  707. if (self::identifyFixedStyleValue($underlineStyles, $styleAttributeValue)) {
  708. $this->styles[$styleID]['font']['underline'] = $styleAttributeValue;
  709. }
  710. break;
  711. }
  712. }
  713. }
  714. /**
  715. * @param $styleID
  716. * @param SimpleXMLElement $styleAttributes
  717. */
  718. private function parseStyleInterior($styleID, SimpleXMLElement $styleAttributes)
  719. {
  720. foreach ($styleAttributes as $styleAttributeKey => $styleAttributeValue) {
  721. switch ($styleAttributeKey) {
  722. case 'Color':
  723. $this->styles[$styleID]['fill']['color']['rgb'] = substr($styleAttributeValue, 1);
  724. break;
  725. case 'Pattern':
  726. $this->styles[$styleID]['fill']['fillType'] = strtolower($styleAttributeValue);
  727. break;
  728. }
  729. }
  730. }
  731. /**
  732. * @param $styleID
  733. * @param SimpleXMLElement $styleAttributes
  734. */
  735. private function parseStyleNumberFormat($styleID, SimpleXMLElement $styleAttributes)
  736. {
  737. $fromFormats = ['\-', '\ '];
  738. $toFormats = ['-', ' '];
  739. foreach ($styleAttributes as $styleAttributeKey => $styleAttributeValue) {
  740. $styleAttributeValue = str_replace($fromFormats, $toFormats, $styleAttributeValue);
  741. switch ($styleAttributeValue) {
  742. case 'Short Date':
  743. $styleAttributeValue = 'dd/mm/yyyy';
  744. break;
  745. }
  746. if ($styleAttributeValue > '') {
  747. $this->styles[$styleID]['numberFormat']['formatCode'] = $styleAttributeValue;
  748. }
  749. }
  750. }
  751. }