Xml.php 34 KB

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