Ods.php 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788
  1. <?php
  2. namespace PhpOffice\PhpSpreadsheet\Reader;
  3. use DateTime;
  4. use DateTimeZone;
  5. use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
  6. use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
  7. use PhpOffice\PhpSpreadsheet\Cell\DataType;
  8. use PhpOffice\PhpSpreadsheet\Document\Properties;
  9. use PhpOffice\PhpSpreadsheet\RichText\RichText;
  10. use PhpOffice\PhpSpreadsheet\Settings;
  11. use PhpOffice\PhpSpreadsheet\Shared\Date;
  12. use PhpOffice\PhpSpreadsheet\Shared\File;
  13. use PhpOffice\PhpSpreadsheet\Spreadsheet;
  14. use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
  15. use XMLReader;
  16. use ZipArchive;
  17. class Ods extends BaseReader
  18. {
  19. /**
  20. * Create a new Ods Reader instance.
  21. */
  22. public function __construct()
  23. {
  24. $this->readFilter = new DefaultReadFilter();
  25. }
  26. /**
  27. * Can the current IReader read the file?
  28. *
  29. * @param string $pFilename
  30. *
  31. * @throws Exception
  32. *
  33. * @return bool
  34. */
  35. public function canRead($pFilename)
  36. {
  37. File::assertFile($pFilename);
  38. $mimeType = 'UNKNOWN';
  39. // Load file
  40. $zip = new ZipArchive();
  41. if ($zip->open($pFilename) === true) {
  42. // check if it is an OOXML archive
  43. $stat = $zip->statName('mimetype');
  44. if ($stat && ($stat['size'] <= 255)) {
  45. $mimeType = $zip->getFromName($stat['name']);
  46. } elseif ($stat = $zip->statName('META-INF/manifest.xml')) {
  47. $xml = simplexml_load_string(
  48. $this->securityScan($zip->getFromName('META-INF/manifest.xml')),
  49. 'SimpleXMLElement',
  50. Settings::getLibXmlLoaderOptions()
  51. );
  52. $namespacesContent = $xml->getNamespaces(true);
  53. if (isset($namespacesContent['manifest'])) {
  54. $manifest = $xml->children($namespacesContent['manifest']);
  55. foreach ($manifest as $manifestDataSet) {
  56. $manifestAttributes = $manifestDataSet->attributes($namespacesContent['manifest']);
  57. if ($manifestAttributes->{'full-path'} == '/') {
  58. $mimeType = (string) $manifestAttributes->{'media-type'};
  59. break;
  60. }
  61. }
  62. }
  63. }
  64. $zip->close();
  65. return $mimeType === 'application/vnd.oasis.opendocument.spreadsheet';
  66. }
  67. return false;
  68. }
  69. /**
  70. * Reads names of the worksheets from a file, without parsing the whole file to a PhpSpreadsheet object.
  71. *
  72. * @param string $pFilename
  73. *
  74. * @throws Exception
  75. *
  76. * @return string[]
  77. */
  78. public function listWorksheetNames($pFilename)
  79. {
  80. File::assertFile($pFilename);
  81. $zip = new ZipArchive();
  82. if (!$zip->open($pFilename)) {
  83. throw new Exception('Could not open ' . $pFilename . ' for reading! Error opening file.');
  84. }
  85. $worksheetNames = [];
  86. $xml = new XMLReader();
  87. $xml->xml(
  88. $this->securityScanFile('zip://' . realpath($pFilename) . '#content.xml'),
  89. null,
  90. Settings::getLibXmlLoaderOptions()
  91. );
  92. $xml->setParserProperty(2, true);
  93. // Step into the first level of content of the XML
  94. $xml->read();
  95. while ($xml->read()) {
  96. // Quickly jump through to the office:body node
  97. while ($xml->name !== 'office:body') {
  98. if ($xml->isEmptyElement) {
  99. $xml->read();
  100. } else {
  101. $xml->next();
  102. }
  103. }
  104. // Now read each node until we find our first table:table node
  105. while ($xml->read()) {
  106. if ($xml->name == 'table:table' && $xml->nodeType == XMLReader::ELEMENT) {
  107. // Loop through each table:table node reading the table:name attribute for each worksheet name
  108. do {
  109. $worksheetNames[] = $xml->getAttribute('table:name');
  110. $xml->next();
  111. } while ($xml->name == 'table:table' && $xml->nodeType == XMLReader::ELEMENT);
  112. }
  113. }
  114. }
  115. return $worksheetNames;
  116. }
  117. /**
  118. * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
  119. *
  120. * @param string $pFilename
  121. *
  122. * @throws Exception
  123. *
  124. * @return array
  125. */
  126. public function listWorksheetInfo($pFilename)
  127. {
  128. File::assertFile($pFilename);
  129. $worksheetInfo = [];
  130. $zip = new ZipArchive();
  131. if (!$zip->open($pFilename)) {
  132. throw new Exception('Could not open ' . $pFilename . ' for reading! Error opening file.');
  133. }
  134. $xml = new XMLReader();
  135. $xml->xml(
  136. $this->securityScanFile('zip://' . realpath($pFilename) . '#content.xml'),
  137. null,
  138. Settings::getLibXmlLoaderOptions()
  139. );
  140. $xml->setParserProperty(2, true);
  141. // Step into the first level of content of the XML
  142. $xml->read();
  143. while ($xml->read()) {
  144. // Quickly jump through to the office:body node
  145. while ($xml->name !== 'office:body') {
  146. if ($xml->isEmptyElement) {
  147. $xml->read();
  148. } else {
  149. $xml->next();
  150. }
  151. }
  152. // Now read each node until we find our first table:table node
  153. while ($xml->read()) {
  154. if ($xml->name == 'table:table' && $xml->nodeType == XMLReader::ELEMENT) {
  155. $worksheetNames[] = $xml->getAttribute('table:name');
  156. $tmpInfo = [
  157. 'worksheetName' => $xml->getAttribute('table:name'),
  158. 'lastColumnLetter' => 'A',
  159. 'lastColumnIndex' => 0,
  160. 'totalRows' => 0,
  161. 'totalColumns' => 0,
  162. ];
  163. // Loop through each child node of the table:table element reading
  164. $currCells = 0;
  165. do {
  166. $xml->read();
  167. if ($xml->name == 'table:table-row' && $xml->nodeType == XMLReader::ELEMENT) {
  168. $rowspan = $xml->getAttribute('table:number-rows-repeated');
  169. $rowspan = empty($rowspan) ? 1 : $rowspan;
  170. $tmpInfo['totalRows'] += $rowspan;
  171. $tmpInfo['totalColumns'] = max($tmpInfo['totalColumns'], $currCells);
  172. $currCells = 0;
  173. // Step into the row
  174. $xml->read();
  175. do {
  176. if ($xml->name == 'table:table-cell' && $xml->nodeType == XMLReader::ELEMENT) {
  177. if (!$xml->isEmptyElement) {
  178. ++$currCells;
  179. $xml->next();
  180. } else {
  181. $xml->read();
  182. }
  183. } elseif ($xml->name == 'table:covered-table-cell' && $xml->nodeType == XMLReader::ELEMENT) {
  184. $mergeSize = $xml->getAttribute('table:number-columns-repeated');
  185. $currCells += (int) $mergeSize;
  186. $xml->read();
  187. }
  188. } while ($xml->name != 'table:table-row');
  189. }
  190. } while ($xml->name != 'table:table');
  191. $tmpInfo['totalColumns'] = max($tmpInfo['totalColumns'], $currCells);
  192. $tmpInfo['lastColumnIndex'] = $tmpInfo['totalColumns'] - 1;
  193. $tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1);
  194. $worksheetInfo[] = $tmpInfo;
  195. }
  196. }
  197. }
  198. return $worksheetInfo;
  199. }
  200. /**
  201. * Loads PhpSpreadsheet from file.
  202. *
  203. * @param string $pFilename
  204. *
  205. * @throws Exception
  206. *
  207. * @return Spreadsheet
  208. */
  209. public function load($pFilename)
  210. {
  211. // Create new Spreadsheet
  212. $spreadsheet = new Spreadsheet();
  213. // Load into this instance
  214. return $this->loadIntoExisting($pFilename, $spreadsheet);
  215. }
  216. /**
  217. * Loads PhpSpreadsheet from file into PhpSpreadsheet instance.
  218. *
  219. * @param string $pFilename
  220. * @param Spreadsheet $spreadsheet
  221. *
  222. * @throws Exception
  223. *
  224. * @return Spreadsheet
  225. */
  226. public function loadIntoExisting($pFilename, Spreadsheet $spreadsheet)
  227. {
  228. File::assertFile($pFilename);
  229. $timezoneObj = new DateTimeZone('Europe/London');
  230. $GMT = new \DateTimeZone('UTC');
  231. $zip = new ZipArchive();
  232. if (!$zip->open($pFilename)) {
  233. throw new Exception('Could not open ' . $pFilename . ' for reading! Error opening file.');
  234. }
  235. // Meta
  236. $xml = simplexml_load_string(
  237. $this->securityScan($zip->getFromName('meta.xml')),
  238. 'SimpleXMLElement',
  239. Settings::getLibXmlLoaderOptions()
  240. );
  241. $namespacesMeta = $xml->getNamespaces(true);
  242. $docProps = $spreadsheet->getProperties();
  243. $officeProperty = $xml->children($namespacesMeta['office']);
  244. foreach ($officeProperty as $officePropertyData) {
  245. $officePropertyDC = [];
  246. if (isset($namespacesMeta['dc'])) {
  247. $officePropertyDC = $officePropertyData->children($namespacesMeta['dc']);
  248. }
  249. foreach ($officePropertyDC as $propertyName => $propertyValue) {
  250. $propertyValue = (string) $propertyValue;
  251. switch ($propertyName) {
  252. case 'title':
  253. $docProps->setTitle($propertyValue);
  254. break;
  255. case 'subject':
  256. $docProps->setSubject($propertyValue);
  257. break;
  258. case 'creator':
  259. $docProps->setCreator($propertyValue);
  260. $docProps->setLastModifiedBy($propertyValue);
  261. break;
  262. case 'date':
  263. $creationDate = strtotime($propertyValue);
  264. $docProps->setCreated($creationDate);
  265. $docProps->setModified($creationDate);
  266. break;
  267. case 'description':
  268. $docProps->setDescription($propertyValue);
  269. break;
  270. }
  271. }
  272. $officePropertyMeta = [];
  273. if (isset($namespacesMeta['dc'])) {
  274. $officePropertyMeta = $officePropertyData->children($namespacesMeta['meta']);
  275. }
  276. foreach ($officePropertyMeta as $propertyName => $propertyValue) {
  277. $propertyValueAttributes = $propertyValue->attributes($namespacesMeta['meta']);
  278. $propertyValue = (string) $propertyValue;
  279. switch ($propertyName) {
  280. case 'initial-creator':
  281. $docProps->setCreator($propertyValue);
  282. break;
  283. case 'keyword':
  284. $docProps->setKeywords($propertyValue);
  285. break;
  286. case 'creation-date':
  287. $creationDate = strtotime($propertyValue);
  288. $docProps->setCreated($creationDate);
  289. break;
  290. case 'user-defined':
  291. $propertyValueType = Properties::PROPERTY_TYPE_STRING;
  292. foreach ($propertyValueAttributes as $key => $value) {
  293. if ($key == 'name') {
  294. $propertyValueName = (string) $value;
  295. } elseif ($key == 'value-type') {
  296. switch ($value) {
  297. case 'date':
  298. $propertyValue = Properties::convertProperty($propertyValue, 'date');
  299. $propertyValueType = Properties::PROPERTY_TYPE_DATE;
  300. break;
  301. case 'boolean':
  302. $propertyValue = Properties::convertProperty($propertyValue, 'bool');
  303. $propertyValueType = Properties::PROPERTY_TYPE_BOOLEAN;
  304. break;
  305. case 'float':
  306. $propertyValue = Properties::convertProperty($propertyValue, 'r4');
  307. $propertyValueType = Properties::PROPERTY_TYPE_FLOAT;
  308. break;
  309. default:
  310. $propertyValueType = Properties::PROPERTY_TYPE_STRING;
  311. }
  312. }
  313. }
  314. $docProps->setCustomProperty($propertyValueName, $propertyValue, $propertyValueType);
  315. break;
  316. }
  317. }
  318. }
  319. // Content
  320. $dom = new \DOMDocument('1.01', 'UTF-8');
  321. $dom->loadXML(
  322. $this->securityScan($zip->getFromName('content.xml')),
  323. Settings::getLibXmlLoaderOptions()
  324. );
  325. $officeNs = $dom->lookupNamespaceUri('office');
  326. $tableNs = $dom->lookupNamespaceUri('table');
  327. $textNs = $dom->lookupNamespaceUri('text');
  328. $xlinkNs = $dom->lookupNamespaceUri('xlink');
  329. $spreadsheets = $dom->getElementsByTagNameNS($officeNs, 'body')
  330. ->item(0)
  331. ->getElementsByTagNameNS($officeNs, 'spreadsheet');
  332. foreach ($spreadsheets as $workbookData) {
  333. /** @var \DOMElement $workbookData */
  334. $tables = $workbookData->getElementsByTagNameNS($tableNs, 'table');
  335. $worksheetID = 0;
  336. foreach ($tables as $worksheetDataSet) {
  337. /** @var \DOMElement $worksheetDataSet */
  338. $worksheetName = $worksheetDataSet->getAttributeNS($tableNs, 'name');
  339. // Check loadSheetsOnly
  340. if (isset($this->loadSheetsOnly)
  341. && $worksheetName
  342. && !in_array($worksheetName, $this->loadSheetsOnly)) {
  343. continue;
  344. }
  345. // Create sheet
  346. if ($worksheetID > 0) {
  347. $spreadsheet->createSheet(); // First sheet is added by default
  348. }
  349. $spreadsheet->setActiveSheetIndex($worksheetID);
  350. if ($worksheetName) {
  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
  353. // bringing the worksheet name in line with the formula, not the reverse
  354. $spreadsheet->getActiveSheet()->setTitle($worksheetName, false, false);
  355. }
  356. // Go through every child of table element
  357. $rowID = 1;
  358. foreach ($worksheetDataSet->childNodes as $childNode) {
  359. /** @var \DOMElement $childNode */
  360. // Filter elements which are not under the "table" ns
  361. if ($childNode->namespaceURI != $tableNs) {
  362. continue;
  363. }
  364. $key = $childNode->nodeName;
  365. // Remove ns from node name
  366. if (strpos($key, ':') !== false) {
  367. $keyChunks = explode(':', $key);
  368. $key = array_pop($keyChunks);
  369. }
  370. switch ($key) {
  371. case 'table-header-rows':
  372. /// TODO :: Figure this out. This is only a partial implementation I guess.
  373. // ($rowData it's not used at all and I'm not sure that PHPExcel
  374. // has an API for this)
  375. // foreach ($rowData as $keyRowData => $cellData) {
  376. // $rowData = $cellData;
  377. // break;
  378. // }
  379. break;
  380. case 'table-row':
  381. if ($childNode->hasAttributeNS($tableNs, 'number-rows-repeated')) {
  382. $rowRepeats = $childNode->getAttributeNS($tableNs, 'number-rows-repeated');
  383. } else {
  384. $rowRepeats = 1;
  385. }
  386. $columnID = 'A';
  387. foreach ($childNode->childNodes as $key => $cellData) {
  388. // @var \DOMElement $cellData
  389. if ($this->getReadFilter() !== null) {
  390. if (!$this->getReadFilter()->readCell($columnID, $rowID, $worksheetName)) {
  391. ++$columnID;
  392. continue;
  393. }
  394. }
  395. // Initialize variables
  396. $formatting = $hyperlink = null;
  397. $hasCalculatedValue = false;
  398. $cellDataFormula = '';
  399. if ($cellData->hasAttributeNS($tableNs, 'formula')) {
  400. $cellDataFormula = $cellData->getAttributeNS($tableNs, 'formula');
  401. $hasCalculatedValue = true;
  402. }
  403. // Annotations
  404. $annotation = $cellData->getElementsByTagNameNS($officeNs, 'annotation');
  405. if ($annotation->length > 0) {
  406. $textNode = $annotation->item(0)->getElementsByTagNameNS($textNs, 'p');
  407. if ($textNode->length > 0) {
  408. $text = $this->scanElementForText($textNode->item(0));
  409. $spreadsheet->getActiveSheet()
  410. ->getComment($columnID . $rowID)
  411. ->setText($this->parseRichText($text));
  412. // ->setAuthor( $author )
  413. }
  414. }
  415. // Content
  416. /** @var \DOMElement[] $paragraphs */
  417. $paragraphs = [];
  418. foreach ($cellData->childNodes as $item) {
  419. /** @var \DOMElement $item */
  420. // Filter text:p elements
  421. if ($item->nodeName == 'text:p') {
  422. $paragraphs[] = $item;
  423. }
  424. }
  425. if (count($paragraphs) > 0) {
  426. // Consolidate if there are multiple p records (maybe with spans as well)
  427. $dataArray = [];
  428. // Text can have multiple text:p and within those, multiple text:span.
  429. // text:p newlines, but text:span does not.
  430. // Also, here we assume there is no text data is span fields are specified, since
  431. // we have no way of knowing proper positioning anyway.
  432. foreach ($paragraphs as $pData) {
  433. $dataArray[] = $this->scanElementForText($pData);
  434. }
  435. $allCellDataText = implode($dataArray, "\n");
  436. $type = $cellData->getAttributeNS($officeNs, 'value-type');
  437. switch ($type) {
  438. case 'string':
  439. $type = DataType::TYPE_STRING;
  440. $dataValue = $allCellDataText;
  441. foreach ($paragraphs as $paragraph) {
  442. $link = $paragraph->getElementsByTagNameNS($textNs, 'a');
  443. if ($link->length > 0) {
  444. $hyperlink = $link->item(0)->getAttributeNS($xlinkNs, 'href');
  445. }
  446. }
  447. break;
  448. case 'boolean':
  449. $type = DataType::TYPE_BOOL;
  450. $dataValue = ($allCellDataText == 'TRUE') ? true : false;
  451. break;
  452. case 'percentage':
  453. $type = DataType::TYPE_NUMERIC;
  454. $dataValue = (float) $cellData->getAttributeNS($officeNs, 'value');
  455. if (floor($dataValue) == $dataValue) {
  456. $dataValue = (int) $dataValue;
  457. }
  458. $formatting = NumberFormat::FORMAT_PERCENTAGE_00;
  459. break;
  460. case 'currency':
  461. $type = DataType::TYPE_NUMERIC;
  462. $dataValue = (float) $cellData->getAttributeNS($officeNs, 'value');
  463. if (floor($dataValue) == $dataValue) {
  464. $dataValue = (int) $dataValue;
  465. }
  466. $formatting = NumberFormat::FORMAT_CURRENCY_USD_SIMPLE;
  467. break;
  468. case 'float':
  469. $type = DataType::TYPE_NUMERIC;
  470. $dataValue = (float) $cellData->getAttributeNS($officeNs, 'value');
  471. if (floor($dataValue) == $dataValue) {
  472. if ($dataValue == (int) $dataValue) {
  473. $dataValue = (int) $dataValue;
  474. } else {
  475. $dataValue = (float) $dataValue;
  476. }
  477. }
  478. break;
  479. case 'date':
  480. $type = DataType::TYPE_NUMERIC;
  481. $value = $cellData->getAttributeNS($officeNs, 'date-value');
  482. $dateObj = new DateTime($value, $GMT);
  483. $dateObj->setTimeZone($timezoneObj);
  484. list($year, $month, $day, $hour, $minute, $second) = explode(
  485. ' ',
  486. $dateObj->format('Y m d H i s')
  487. );
  488. $dataValue = Date::formattedPHPToExcel(
  489. $year,
  490. $month,
  491. $day,
  492. $hour,
  493. $minute,
  494. $second
  495. );
  496. if ($dataValue != floor($dataValue)) {
  497. $formatting = NumberFormat::FORMAT_DATE_XLSX15
  498. . ' '
  499. . NumberFormat::FORMAT_DATE_TIME4;
  500. } else {
  501. $formatting = NumberFormat::FORMAT_DATE_XLSX15;
  502. }
  503. break;
  504. case 'time':
  505. $type = DataType::TYPE_NUMERIC;
  506. $timeValue = $cellData->getAttributeNS($officeNs, 'time-value');
  507. $dataValue = Date::PHPToExcel(
  508. strtotime(
  509. '01-01-1970 ' . implode(':', sscanf($timeValue, 'PT%dH%dM%dS'))
  510. )
  511. );
  512. $formatting = NumberFormat::FORMAT_DATE_TIME4;
  513. break;
  514. default:
  515. $dataValue = null;
  516. }
  517. } else {
  518. $type = DataType::TYPE_NULL;
  519. $dataValue = null;
  520. }
  521. if ($hasCalculatedValue) {
  522. $type = DataType::TYPE_FORMULA;
  523. $cellDataFormula = substr($cellDataFormula, strpos($cellDataFormula, ':=') + 1);
  524. $temp = explode('"', $cellDataFormula);
  525. $tKey = false;
  526. foreach ($temp as &$value) {
  527. // Only replace in alternate array entries (i.e. non-quoted blocks)
  528. if ($tKey = !$tKey) {
  529. // Cell range reference in another sheet
  530. $value = preg_replace('/\[([^\.]+)\.([^\.]+):\.([^\.]+)\]/U', '$1!$2:$3', $value);
  531. // Cell reference in another sheet
  532. $value = preg_replace('/\[([^\.]+)\.([^\.]+)\]/U', '$1!$2', $value);
  533. // Cell range reference
  534. $value = preg_replace('/\[\.([^\.]+):\.([^\.]+)\]/U', '$1:$2', $value);
  535. // Simple cell reference
  536. $value = preg_replace('/\[\.([^\.]+)\]/U', '$1', $value);
  537. $value = Calculation::translateSeparator(';', ',', $value, $inBraces);
  538. }
  539. }
  540. unset($value);
  541. // Then rebuild the formula string
  542. $cellDataFormula = implode('"', $temp);
  543. }
  544. if ($cellData->hasAttributeNS($tableNs, 'number-columns-repeated')) {
  545. $colRepeats = (int) $cellData->getAttributeNS($tableNs, 'number-columns-repeated');
  546. } else {
  547. $colRepeats = 1;
  548. }
  549. if ($type !== null) {
  550. for ($i = 0; $i < $colRepeats; ++$i) {
  551. if ($i > 0) {
  552. ++$columnID;
  553. }
  554. if ($type !== DataType::TYPE_NULL) {
  555. for ($rowAdjust = 0; $rowAdjust < $rowRepeats; ++$rowAdjust) {
  556. $rID = $rowID + $rowAdjust;
  557. $cell = $spreadsheet->getActiveSheet()
  558. ->getCell($columnID . $rID);
  559. // Set value
  560. if ($hasCalculatedValue) {
  561. $cell->setValueExplicit($cellDataFormula, $type);
  562. } else {
  563. $cell->setValueExplicit($dataValue, $type);
  564. }
  565. if ($hasCalculatedValue) {
  566. $cell->setCalculatedValue($dataValue);
  567. }
  568. // Set other properties
  569. if ($formatting !== null) {
  570. $spreadsheet->getActiveSheet()
  571. ->getStyle($columnID . $rID)
  572. ->getNumberFormat()
  573. ->setFormatCode($formatting);
  574. } else {
  575. $spreadsheet->getActiveSheet()
  576. ->getStyle($columnID . $rID)
  577. ->getNumberFormat()
  578. ->setFormatCode(NumberFormat::FORMAT_GENERAL);
  579. }
  580. if ($hyperlink !== null) {
  581. $cell->getHyperlink()
  582. ->setUrl($hyperlink);
  583. }
  584. }
  585. }
  586. }
  587. }
  588. // Merged cells
  589. if ($cellData->hasAttributeNS($tableNs, 'number-columns-spanned')
  590. || $cellData->hasAttributeNS($tableNs, 'number-rows-spanned')
  591. ) {
  592. if (($type !== DataType::TYPE_NULL) || (!$this->readDataOnly)) {
  593. $columnTo = $columnID;
  594. if ($cellData->hasAttributeNS($tableNs, 'number-columns-spanned')) {
  595. $columnIndex = Coordinate::columnIndexFromString($columnID);
  596. $columnIndex += (int) $cellData->getAttributeNS($tableNs, 'number-columns-spanned');
  597. $columnIndex -= 2;
  598. $columnTo = Coordinate::stringFromColumnIndex($columnIndex + 1);
  599. }
  600. $rowTo = $rowID;
  601. if ($cellData->hasAttributeNS($tableNs, 'number-rows-spanned')) {
  602. $rowTo = $rowTo + (int) $cellData->getAttributeNS($tableNs, 'number-rows-spanned') - 1;
  603. }
  604. $cellRange = $columnID . $rowID . ':' . $columnTo . $rowTo;
  605. $spreadsheet->getActiveSheet()->mergeCells($cellRange);
  606. }
  607. }
  608. ++$columnID;
  609. }
  610. $rowID += $rowRepeats;
  611. break;
  612. }
  613. }
  614. ++$worksheetID;
  615. }
  616. }
  617. // Return
  618. return $spreadsheet;
  619. }
  620. /**
  621. * Recursively scan element.
  622. *
  623. * @param \DOMNode $element
  624. *
  625. * @return string
  626. */
  627. protected function scanElementForText(\DOMNode $element)
  628. {
  629. $str = '';
  630. foreach ($element->childNodes as $child) {
  631. /** @var \DOMNode $child */
  632. if ($child->nodeType == XML_TEXT_NODE) {
  633. $str .= $child->nodeValue;
  634. } elseif ($child->nodeType == XML_ELEMENT_NODE && $child->nodeName == 'text:s') {
  635. // It's a space
  636. // Multiple spaces?
  637. /** @var \DOMAttr $cAttr */
  638. $cAttr = $child->attributes->getNamedItem('c');
  639. if ($cAttr) {
  640. $multiplier = (int) $cAttr->nodeValue;
  641. } else {
  642. $multiplier = 1;
  643. }
  644. $str .= str_repeat(' ', $multiplier);
  645. }
  646. if ($child->hasChildNodes()) {
  647. $str .= $this->scanElementForText($child);
  648. }
  649. }
  650. return $str;
  651. }
  652. /**
  653. * @param string $is
  654. *
  655. * @return RichText
  656. */
  657. private function parseRichText($is)
  658. {
  659. $value = new RichText();
  660. $value->createText($is);
  661. return $value;
  662. }
  663. }