Ods.php 34 KB

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