diff --git a/src/PhpSpreadsheet/Reader/IReadFilter.php b/src/PhpSpreadsheet/Reader/IReadFilter.php index a4131c09..75b613f3 100644 --- a/src/PhpSpreadsheet/Reader/IReadFilter.php +++ b/src/PhpSpreadsheet/Reader/IReadFilter.php @@ -29,9 +29,9 @@ interface IReadFilter /** * Should this cell be read? * - * @param $column Column address (as a string value like "A", or "IV") - * @param $row Row number - * @param $worksheetName Optional worksheet name + * @param $column string Column address (as a string value like "A", or "IV") + * @param $row int Row number + * @param $worksheetName string Optional worksheet name * * @return bool */ diff --git a/src/PhpSpreadsheet/Reader/Ods.php b/src/PhpSpreadsheet/Reader/Ods.php index 09d539b0..cf514883 100644 --- a/src/PhpSpreadsheet/Reader/Ods.php +++ b/src/PhpSpreadsheet/Reader/Ods.php @@ -4,7 +4,10 @@ namespace PhpOffice\PhpSpreadsheet\Reader; use DateTime; use DateTimeZone; +use PhpOffice\PhpSpreadsheet\Calculation; +use PhpOffice\PhpSpreadsheet\Cell\DataType; use PhpOffice\PhpSpreadsheet\Shared\File; +use PhpOffice\PhpSpreadsheet\Style\NumberFormat; /** * Copyright (c) 2006 - 2016 PhpSpreadsheet. @@ -61,7 +64,10 @@ class Ods extends BaseReader implements IReader $zipClass = \PhpOffice\PhpSpreadsheet\Settings::getZipClass(); $mimeType = 'UNKNOWN'; + // Load file + + /** @var \ZipArchive $zip */ $zip = new $zipClass(); if ($zip->open($pFilename) === true) { // check if it is an OOXML archive @@ -101,6 +107,8 @@ class Ods extends BaseReader implements IReader * @param string $pFilename * * @throws Exception + * + * @return string[] */ public function listWorksheetNames($pFilename) { @@ -108,6 +116,7 @@ class Ods extends BaseReader implements IReader $zipClass = \PhpOffice\PhpSpreadsheet\Settings::getZipClass(); + /** @var \ZipArchive $zip */ $zip = new $zipClass(); if (!$zip->open($pFilename)) { throw new Exception('Could not open ' . $pFilename . ' for reading! Error opening file.'); @@ -115,18 +124,18 @@ class Ods extends BaseReader implements IReader $worksheetNames = []; - $xml = new XMLReader(); - $res = $xml->xml( + $xml = new \XMLReader(); + $xml->xml( $this->securityScanFile('zip://' . realpath($pFilename) . '#content.xml'), null, \PhpOffice\PhpSpreadsheet\Settings::getLibXmlLoaderOptions() ); $xml->setParserProperty(2, true); - // Step into the first level of content of the XML + // Step into the first level of content of the XML $xml->read(); while ($xml->read()) { - // Quickly jump through to the office:body node + // Quickly jump through to the office:body node while ($xml->name !== 'office:body') { if ($xml->isEmptyElement) { $xml->read(); @@ -134,14 +143,14 @@ class Ods extends BaseReader implements IReader $xml->next(); } } - // Now read each node until we find our first table:table node + // Now read each node until we find our first table:table node while ($xml->read()) { - if ($xml->name == 'table:table' && $xml->nodeType == XMLReader::ELEMENT) { - // Loop through each table:table node reading the table:name attribute for each worksheet name + if ($xml->name == 'table:table' && $xml->nodeType == \XMLReader::ELEMENT) { + // Loop through each table:table node reading the table:name attribute for each worksheet name do { $worksheetNames[] = $xml->getAttribute('table:name'); $xml->next(); - } while ($xml->name == 'table:table' && $xml->nodeType == XMLReader::ELEMENT); + } while ($xml->name == 'table:table' && $xml->nodeType == \XMLReader::ELEMENT); } } } @@ -155,6 +164,8 @@ class Ods extends BaseReader implements IReader * @param string $pFilename * * @throws Exception + * + * @return array */ public function listWorksheetInfo($pFilename) { @@ -164,12 +175,13 @@ class Ods extends BaseReader implements IReader $zipClass = \PhpOffice\PhpSpreadsheet\Settings::getZipClass(); + /** @var \ZipArchive $zip */ $zip = new $zipClass(); if (!$zip->open($pFilename)) { throw new Exception('Could not open ' . $pFilename . ' for reading! Error opening file.'); } - $xml = new XMLReader(); + $xml = new \XMLReader(); $res = $xml->xml( $this->securityScanFile('zip://' . realpath($pFilename) . '#content.xml'), null, @@ -177,10 +189,10 @@ class Ods extends BaseReader implements IReader ); $xml->setParserProperty(2, true); - // Step into the first level of content of the XML + // Step into the first level of content of the XML $xml->read(); while ($xml->read()) { - // Quickly jump through to the office:body node + // Quickly jump through to the office:body node while ($xml->name !== 'office:body') { if ($xml->isEmptyElement) { $xml->read(); @@ -188,9 +200,9 @@ class Ods extends BaseReader implements IReader $xml->next(); } } - // Now read each node until we find our first table:table node + // Now read each node until we find our first table:table node while ($xml->read()) { - if ($xml->name == 'table:table' && $xml->nodeType == XMLReader::ELEMENT) { + if ($xml->name == 'table:table' && $xml->nodeType == \XMLReader::ELEMENT) { $worksheetNames[] = $xml->getAttribute('table:name'); $tmpInfo = [ @@ -201,27 +213,27 @@ class Ods extends BaseReader implements IReader 'totalColumns' => 0, ]; - // Loop through each child node of the table:table element reading + // Loop through each child node of the table:table element reading $currCells = 0; do { $xml->read(); - if ($xml->name == 'table:table-row' && $xml->nodeType == XMLReader::ELEMENT) { + if ($xml->name == 'table:table-row' && $xml->nodeType == \XMLReader::ELEMENT) { $rowspan = $xml->getAttribute('table:number-rows-repeated'); $rowspan = empty($rowspan) ? 1 : $rowspan; $tmpInfo['totalRows'] += $rowspan; $tmpInfo['totalColumns'] = max($tmpInfo['totalColumns'], $currCells); $currCells = 0; - // Step into the row + // Step into the row $xml->read(); do { - if ($xml->name == 'table:table-cell' && $xml->nodeType == XMLReader::ELEMENT) { + if ($xml->name == 'table:table-cell' && $xml->nodeType == \XMLReader::ELEMENT) { if (!$xml->isEmptyElement) { ++$currCells; $xml->next(); } else { $xml->read(); } - } elseif ($xml->name == 'table:covered-table-cell' && $xml->nodeType == XMLReader::ELEMENT) { + } elseif ($xml->name == 'table:covered-table-cell' && $xml->nodeType == \XMLReader::ELEMENT) { $mergeSize = $xml->getAttribute('table:number-columns-repeated'); $currCells += $mergeSize; $xml->read(); @@ -292,11 +304,16 @@ class Ods extends BaseReader implements IReader $zipClass = \PhpOffice\PhpSpreadsheet\Settings::getZipClass(); + /** @var \ZipArchive $zip */ $zip = new $zipClass(); if (!$zip->open($pFilename)) { throw new Exception('Could not open ' . $pFilename . ' for reading! Error opening file.'); } + /* + * Meta + */ + $xml = simplexml_load_string( $this->securityScan($zip->getFromName('meta.xml')), 'SimpleXMLElement', @@ -382,50 +399,94 @@ class Ods extends BaseReader implements IReader } } - $xml = simplexml_load_string( + /* + * Content + */ + + $dom = new \DOMDocument('1.01', 'UTF-8'); + $dom->loadXML( $this->securityScan($zip->getFromName('content.xml')), - 'SimpleXMLElement', \PhpOffice\PhpSpreadsheet\Settings::getLibXmlLoaderOptions() ); - $namespacesContent = $xml->getNamespaces(true); - $workbook = $xml->children($namespacesContent['office']); - foreach ($workbook->body->spreadsheet as $workbookData) { - $workbookData = $workbookData->children($namespacesContent['table']); + $officeNs = $dom->lookupNamespaceUri('office'); + $tableNs = $dom->lookupNamespaceUri('table'); + $textNs = $dom->lookupNamespaceUri('text'); + $xlinkNs = $dom->lookupNamespaceUri('xlink'); + + $spreadsheets = $dom->getElementsByTagNameNS($officeNs, 'body') + ->item(0) + ->getElementsByTagNameNS($officeNs, 'spreadsheet'); + + foreach ($spreadsheets as $workbookData) { + /** @var \DOMElement $workbookData */ + $tables = $workbookData->getElementsByTagNameNS($tableNs, 'table'); + $worksheetID = 0; - foreach ($workbookData->table as $worksheetDataSet) { - $worksheetData = $worksheetDataSet->children($namespacesContent['table']); - $worksheetDataAttributes = $worksheetDataSet->attributes($namespacesContent['table']); - if ((isset($this->loadSheetsOnly)) && (isset($worksheetDataAttributes['name'])) && - (!in_array($worksheetDataAttributes['name'], $this->loadSheetsOnly))) { + foreach ($tables as $worksheetDataSet) { + /** @var \DOMElement $worksheetDataSet */ + $worksheetName = $worksheetDataSet->getAttributeNS($tableNs, 'name'); + + // Check loadSheetsOnly + if (isset($this->loadSheetsOnly) + && $worksheetName + && !in_array($worksheetName, $this->loadSheetsOnly)) { continue; } - // Create new Worksheet - $spreadsheet->createSheet(); + // Create sheet + if ($worksheetID > 0) { + $spreadsheet->createSheet(); // First sheet is added by default + } $spreadsheet->setActiveSheetIndex($worksheetID); - if (isset($worksheetDataAttributes['name'])) { - $worksheetName = (string) $worksheetDataAttributes['name']; - // Use false for $updateFormulaCellReferences to prevent adjustment of worksheet references in - // formula cells... during the load, all formulae should be correct, and we're simply - // bringing the worksheet name in line with the formula, not the reverse + + if ($worksheetName) { + // Use false for $updateFormulaCellReferences to prevent adjustment of worksheet references in + // formula cells... during the load, all formulae should be correct, and we're simply + // bringing the worksheet name in line with the formula, not the reverse $spreadsheet->getActiveSheet()->setTitle($worksheetName, false); } + // Go through every child of table element $rowID = 1; - foreach ($worksheetData as $key => $rowData) { + foreach ($worksheetDataSet->childNodes as $childNode) { + /** @var \DOMElement $childNode */ + + // Filter elements which are not under the "table" ns + if ($childNode->namespaceURI != $tableNs) { + continue; + } + + $key = $childNode->nodeName; + + // Remove ns from node name + if (strpos($key, ':') !== false) { + $keyChunks = explode(':', $key); + $key = array_pop($keyChunks); + } + switch ($key) { case 'table-header-rows': - foreach ($rowData as $keyRowData => $cellData) { - $rowData = $cellData; - break; - } + /// TODO :: Figure this out. This is only a partial implementation I guess. + // ($rowData it's not used at all and I'm not sure that PHPExcel + // has an API for this) + +// foreach ($rowData as $keyRowData => $cellData) { +// $rowData = $cellData; +// break; +// } break; case 'table-row': - $rowDataTableAttributes = $rowData->attributes($namespacesContent['table']); - $rowRepeats = (isset($rowDataTableAttributes['number-rows-repeated'])) ? $rowDataTableAttributes['number-rows-repeated'] : 1; + if ($childNode->hasAttributeNS($tableNs, 'number-rows-repeated')) { + $rowRepeats = $childNode->getAttributeNS($tableNs, 'number-rows-repeated'); + } else { + $rowRepeats = 1; + } + $columnID = 'A'; - foreach ($rowData as $key => $cellData) { + foreach ($childNode->childNodes as $key => $cellData) { + /* @var \DOMElement $cellData */ + if ($this->getReadFilter() !== null) { if (!$this->getReadFilter()->readCell($columnID, $rowID, $worksheetName)) { ++$columnID; @@ -433,93 +494,101 @@ class Ods extends BaseReader implements IReader } } - $cellDataText = (isset($namespacesContent['text'])) ? $cellData->children($namespacesContent['text']) : ''; - $cellDataOffice = $cellData->children($namespacesContent['office']); - $cellDataOfficeAttributes = $cellData->attributes($namespacesContent['office']); - $cellDataTableAttributes = $cellData->attributes($namespacesContent['table']); - - $type = $formatting = $hyperlink = null; + // Initialize variables + $formatting = $hyperlink = null; $hasCalculatedValue = false; $cellDataFormula = ''; - if (isset($cellDataTableAttributes['formula'])) { - $cellDataFormula = $cellDataTableAttributes['formula']; + + if ($cellData->hasAttributeNS($tableNs, 'formula')) { + $cellDataFormula = $cellData->getAttributeNS($tableNs, 'formula'); $hasCalculatedValue = true; } - if (isset($cellDataOffice->annotation)) { - $annotationText = $cellDataOffice->annotation->children($namespacesContent['text']); - $textArray = []; - foreach ($annotationText as $t) { - if (isset($t->span)) { - foreach ($t->span as $text) { - $textArray[] = (string) $text; - } - } else { - $textArray[] = (string) $t; - } - } - $text = implode("\n", $textArray); - $spreadsheet->getActiveSheet()->getComment($columnID . $rowID)->setText($this->parseRichText($text)); + // Annotations + $annotation = $cellData->getElementsByTagNameNS($officeNs, 'annotation'); + + if ($annotation->length > 0) { + $textNode = $annotation->item(0)->getElementsByTagNameNS($textNs, 'p'); + + if ($textNode->length > 0) { + $text = $this->scanElementForText($textNode->item(0)); + + $spreadsheet->getActiveSheet() + ->getComment($columnID . $rowID) + ->setText($this->parseRichText($text)); // ->setAuthor( $author ) + } } - if (isset($cellDataText->p)) { + // Content + + /** @var \DOMElement[] $paragraphs */ + $paragraphs = []; + + foreach ($cellData->childNodes as $item) { + /** @var \DOMElement $item */ + + // Filter text:p elements + if ($item->nodeName == 'text:p') { + $paragraphs[] = $item; + } + } + + if (count($paragraphs) > 0) { // Consolidate if there are multiple p records (maybe with spans as well) $dataArray = []; + // Text can have multiple text:p and within those, multiple text:span. // text:p newlines, but text:span does not. // Also, here we assume there is no text data is span fields are specified, since // we have no way of knowing proper positioning anyway. - foreach ($cellDataText->p as $pData) { - if (isset($pData->span)) { - // span sections do not newline, so we just create one large string here - $spanSection = ''; - foreach ($pData->span as $spanData) { - $spanSection .= $spanData; - } - array_push($dataArray, $spanSection); - } elseif (isset($pData->a)) { - //Reading the hyperlinks in p - array_push($dataArray, $pData->a); - } else { - array_push($dataArray, $pData); - } + + foreach ($paragraphs as $pData) { + $dataArray[] = $this->scanElementForText($pData); } $allCellDataText = implode($dataArray, "\n"); - switch ($cellDataOfficeAttributes['value-type']) { + $type = $cellData->getAttributeNS($officeNs, 'value-type'); + + switch ($type) { case 'string': - $type = \PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_STRING; + $type = DataType::TYPE_STRING; $dataValue = $allCellDataText; - if (isset($dataValue->a)) { - $dataValue = $dataValue->a; - $cellXLinkAttributes = $dataValue->attributes($namespacesContent['xlink']); - $hyperlink = $cellXLinkAttributes['href']; + + foreach ($paragraphs as $paragraph) { + $link = $paragraph->getElementsByTagNameNS($textNs, 'a'); + if ($link->length > 0) { + $hyperlink = $link->item(0)->getAttributeNS($xlinkNs, 'href'); + } } + break; case 'boolean': - $type = \PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_BOOL; + $type = DataType::TYPE_BOOL; $dataValue = ($allCellDataText == 'TRUE') ? true : false; break; case 'percentage': - $type = \PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_NUMERIC; - $dataValue = (float) $cellDataOfficeAttributes['value']; + $type = DataType::TYPE_NUMERIC; + $dataValue = (float) $cellData->getAttributeNS($officeNs, 'value'); + if (floor($dataValue) == $dataValue) { $dataValue = (int) $dataValue; } $formatting = \PhpOffice\PhpSpreadsheet\Style\NumberFormat::FORMAT_PERCENTAGE_00; break; case 'currency': - $type = \PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_NUMERIC; - $dataValue = (float) $cellDataOfficeAttributes['value']; + $type = DataType::TYPE_NUMERIC; + $dataValue = (float) $cellData->getAttributeNS($officeNs, 'value'); + if (floor($dataValue) == $dataValue) { $dataValue = (int) $dataValue; } $formatting = \PhpOffice\PhpSpreadsheet\Style\NumberFormat::FORMAT_CURRENCY_USD_SIMPLE; break; case 'float': - $type = \PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_NUMERIC; - $dataValue = (float) $cellDataOfficeAttributes['value']; + $type = DataType::TYPE_NUMERIC; + $dataValue = (float) $cellData->getAttributeNS($officeNs, 'value'); + if (floor($dataValue) == $dataValue) { if ($dataValue == (int) $dataValue) { $dataValue = (int) $dataValue; @@ -529,85 +598,155 @@ class Ods extends BaseReader implements IReader } break; case 'date': - $type = \PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_NUMERIC; - $dateObj = new DateTime($cellDataOfficeAttributes['date-value'], $GMT); + $type = DataType::TYPE_NUMERIC; + $value = $cellData->getAttributeNS($officeNs, 'date-value'); + + $dateObj = new DateTime($value, $GMT); $dateObj->setTimeZone($timezoneObj); - list($year, $month, $day, $hour, $minute, $second) = explode(' ', $dateObj->format('Y m d H i s')); - $dataValue = \PhpOffice\PhpSpreadsheet\Shared\Date::formattedPHPToExcel($year, $month, $day, $hour, $minute, $second); + list($year, $month, $day, $hour, $minute, $second) = explode( + ' ', + $dateObj->format('Y m d H i s') + ); + + $dataValue = \PhpOffice\PhpSpreadsheet\Shared\Date::formattedPHPToExcel( + $year, + $month, + $day, + $hour, + $minute, + $second + ); + if ($dataValue != floor($dataValue)) { - $formatting = \PhpOffice\PhpSpreadsheet\Style\NumberFormat::FORMAT_DATE_XLSX15 . ' ' . \PhpOffice\PhpSpreadsheet\Style\NumberFormat::FORMAT_DATE_TIME4; + $formatting = \PhpOffice\PhpSpreadsheet\Style\NumberFormat::FORMAT_DATE_XLSX15 + . ' ' + . \PhpOffice\PhpSpreadsheet\Style\NumberFormat::FORMAT_DATE_TIME4; } else { $formatting = \PhpOffice\PhpSpreadsheet\Style\NumberFormat::FORMAT_DATE_XLSX15; } break; case 'time': - $type = \PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_NUMERIC; - $dataValue = \PhpOffice\PhpSpreadsheet\Shared\Date::PHPToExcel(strtotime('01-01-1970 ' . implode(':', sscanf($cellDataOfficeAttributes['time-value'], 'PT%dH%dM%dS')))); + $type = DataType::TYPE_NUMERIC; + + $timeValue = $cellData->getAttributeNS($officeNs, 'time-value'); + + $dataValue = \PhpOffice\PhpSpreadsheet\Shared\Date::PHPToExcel( + strtotime( + '01-01-1970 ' . implode(':', sscanf($timeValue, 'PT%dH%dM%dS')) + ) + ); $formatting = \PhpOffice\PhpSpreadsheet\Style\NumberFormat::FORMAT_DATE_TIME4; break; + default: + $dataValue = null; } } else { - $type = \PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_NULL; + $type = DataType::TYPE_NULL; $dataValue = null; } if ($hasCalculatedValue) { - $type = \PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_FORMULA; + $type = DataType::TYPE_FORMULA; $cellDataFormula = substr($cellDataFormula, strpos($cellDataFormula, ':=') + 1); $temp = explode('"', $cellDataFormula); $tKey = false; foreach ($temp as &$value) { - // Only replace in alternate array entries (i.e. non-quoted blocks) + // Only replace in alternate array entries (i.e. non-quoted blocks) if ($tKey = !$tKey) { - $value = preg_replace('/\[([^\.]+)\.([^\.]+):\.([^\.]+)\]/Ui', '$1!$2:$3', $value); // Cell range reference in another sheet - $value = preg_replace('/\[([^\.]+)\.([^\.]+)\]/Ui', '$1!$2', $value); // Cell reference in another sheet - $value = preg_replace('/\[\.([^\.]+):\.([^\.]+)\]/Ui', '$1:$2', $value); // Cell range reference - $value = preg_replace('/\[\.([^\.]+)\]/Ui', '$1', $value); // Simple cell reference - $value = \PhpOffice\PhpSpreadsheet\Calculation::translateSeparator(';', ',', $value, $inBraces); + // Cell range reference in another sheet + $value = preg_replace('/\[([^\.]+)\.([^\.]+):\.([^\.]+)\]/Ui', '$1!$2:$3', $value); + + // Cell reference in another sheet + $value = preg_replace('/\[([^\.]+)\.([^\.]+)\]/Ui', '$1!$2', $value); + + // Cell range reference + $value = preg_replace('/\[\.([^\.]+):\.([^\.]+)\]/Ui', '$1:$2', $value); + + // Simple cell reference + $value = preg_replace('/\[\.([^\.]+)\]/Ui', '$1', $value); + + $value = Calculation::translateSeparator(';', ',', $value, $inBraces); } } unset($value); - // Then rebuild the formula string + + // Then rebuild the formula string $cellDataFormula = implode('"', $temp); } - $colRepeats = (isset($cellDataTableAttributes['number-columns-repeated'])) ? $cellDataTableAttributes['number-columns-repeated'] : 1; + if ($cellData->hasAttributeNS($tableNs, 'number-columns-repeated')) { + $colRepeats = (int) $cellData->getAttributeNS($tableNs, 'number-columns-repeated'); + } else { + $colRepeats = 1; + } + if ($type !== null) { for ($i = 0; $i < $colRepeats; ++$i) { if ($i > 0) { ++$columnID; } - if ($type !== \PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_NULL) { + + if ($type !== DataType::TYPE_NULL) { for ($rowAdjust = 0; $rowAdjust < $rowRepeats; ++$rowAdjust) { $rID = $rowID + $rowAdjust; - $spreadsheet->getActiveSheet()->getCell($columnID . $rID)->setValueExplicit((($hasCalculatedValue) ? $cellDataFormula : $dataValue), $type); + + $cell = $spreadsheet->getActiveSheet() + ->getCell($columnID . $rID); + + // Set value if ($hasCalculatedValue) { - $spreadsheet->getActiveSheet()->getCell($columnID . $rID)->setCalculatedValue($dataValue); - } - if ($formatting !== null) { - $spreadsheet->getActiveSheet()->getStyle($columnID . $rID)->getNumberFormat()->setFormatCode($formatting); + $cell->setValueExplicit($cellDataFormula, $type); } else { - $spreadsheet->getActiveSheet()->getStyle($columnID . $rID)->getNumberFormat()->setFormatCode(\PhpOffice\PhpSpreadsheet\Style\NumberFormat::FORMAT_GENERAL); + $cell->setValueExplicit($dataValue, $type); } + + if ($hasCalculatedValue) { + $cell->setCalculatedValue($dataValue); + } + + // Set other properties + if ($formatting !== null) { + $spreadsheet->getActiveSheet() + ->getStyle($columnID . $rID) + ->getNumberFormat() + ->setFormatCode($formatting); + } else { + $spreadsheet->getActiveSheet() + ->getStyle($columnID . $rID) + ->getNumberFormat() + ->setFormatCode(NumberFormat::FORMAT_GENERAL); + } + if ($hyperlink !== null) { - $spreadsheet->getActiveSheet()->getCell($columnID . $rID)->getHyperlink()->setUrl($hyperlink); + $cell->getHyperlink() + ->setUrl($hyperlink); } } } } } - // Merged cells - if ((isset($cellDataTableAttributes['number-columns-spanned'])) || (isset($cellDataTableAttributes['number-rows-spanned']))) { - if (($type !== \PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_NULL) || (!$this->readDataOnly)) { + // Merged cells + if ($childNode->hasAttributeNS($tableNs, 'number-columns-spanned') + || $childNode->hasAttributeNS($tableNs, 'number-rows-spanned') + ) { + if (($type !== DataType::TYPE_NULL) || (!$this->readDataOnly)) { $columnTo = $columnID; - if (isset($cellDataTableAttributes['number-columns-spanned'])) { - $columnTo = \PhpOffice\PhpSpreadsheet\Cell::stringFromColumnIndex(\PhpOffice\PhpSpreadsheet\Cell::columnIndexFromString($columnID) + $cellDataTableAttributes['number-columns-spanned'] - 2); + + if ($cellData->hasAttributeNS($tableNs, 'number-columns-spanned')) { + $columnIndex = \PhpOffice\PhpSpreadsheet\Cell::columnIndexFromString($columnID); + $columnIndex += (int) $cellData->getAttributeNS($tableNs, 'number-columns-spanned'); + $columnIndex -= 2; + + $columnTo = \PhpOffice\PhpSpreadsheet\Cell::stringFromColumnIndex($columnIndex); } + $rowTo = $rowID; - if (isset($cellDataTableAttributes['number-rows-spanned'])) { - $rowTo = $rowTo + $cellDataTableAttributes['number-rows-spanned'] - 1; + + if ($cellData->hasAttributeNS($tableNs, 'number-rows-spanned')) { + $rowTo = $rowTo + (int) $cellData->getAttributeNS($tableNs, 'number-rows-spanned') - 1; } + $cellRange = $columnID . $rowID . ':' . $columnTo . $rowTo; $spreadsheet->getActiveSheet()->mergeCells($cellRange); } @@ -627,10 +766,51 @@ class Ods extends BaseReader implements IReader return $spreadsheet; } + /** + * Recursively scan element. + * + * @param \DOMNode $element + * + * @return string + */ + protected function scanElementForText(\DOMNode $element) + { + $str = ''; + foreach ($element->childNodes as $child) { + /** @var \DOMNode $child */ + if ($child->nodeType == XML_TEXT_NODE) { + $str .= $child->nodeValue; + } elseif ($child->nodeType == XML_ELEMENT_NODE && $child->nodeName == 'text:s') { + // It's a space + + // Multiple spaces? + if (isset($child->attributes['text:c'])) { + /** @var \DOMAttr $cAttr */ + $cAttr = $child->attributes['text:c']; + $multiplier = (int) $cAttr->nodeValue; + } else { + $multiplier = 1; + } + + $str .= str_repeat(' ', $multiplier); + } + + if ($child->hasChildNodes()) { + $str .= $this->scanElementForText($child); + } + } + + return $str; + } + + /** + * @param string $is + * + * @return \PhpOffice\PhpSpreadsheet\RichText + */ private function parseRichText($is = '') { $value = new \PhpOffice\PhpSpreadsheet\RichText(); - $value->createText($is); return $value; diff --git a/tests/PhpSpreadsheetTests/Reader/OdsTest.php b/tests/PhpSpreadsheetTests/Reader/OdsTest.php new file mode 100644 index 00000000..4b958f9c --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/OdsTest.php @@ -0,0 +1,227 @@ +spreadsheetOOCalcTest) { + $filename = __DIR__ . '/../../../samples/templates/OOCalcTest.ods'; + + // Load into this instance + $reader = new Ods(); + $this->spreadsheetOOCalcTest = $reader->loadIntoExisting($filename, new \PhpOffice\PhpSpreadsheet\Spreadsheet()); + } + + return $this->spreadsheetOOCalcTest; + } + + /** + * @return \PhpOffice\PhpSpreadsheet\Spreadsheet + */ + protected function loadDataFile() + { + if (!$this->spreadsheetData) { + $filename = __DIR__ . '/../../data/Reader/Ods/data.ods'; + + // Load into this instance + $reader = new Ods(); + $this->spreadsheetData = $reader->loadIntoExisting($filename, new \PhpOffice\PhpSpreadsheet\Spreadsheet()); + } + + return $this->spreadsheetData; + } + + public function testReadFileProperties() + { + $filename = __DIR__ . '/../../data/Reader/Ods/data.ods'; + + // Load into this instance + $reader = new Ods(); + + // Test "listWorksheetNames" method + + $this->assertEquals([ + 'Sheet1', + 'Second Sheet', + ], $reader->listWorksheetNames($filename)); + } + + public function testLoadWorksheets() + { + $spreadsheet = $this->loadDataFile(); + + $this->assertInstanceOf('PhpOffice\PhpSpreadsheet\Spreadsheet', $spreadsheet); + + $this->assertEquals(2, $spreadsheet->getSheetCount()); + + $firstSheet = $spreadsheet->getSheet(0); + $this->assertInstanceOf('PhpOffice\PhpSpreadsheet\Worksheet', $firstSheet); + + $secondSheet = $spreadsheet->getSheet(1); + $this->assertInstanceOf('PhpOffice\PhpSpreadsheet\Worksheet', $secondSheet); + } + + public function testReadValueAndComments() + { + $spreadsheet = $this->loadOOCalcTestFile(); + + $firstSheet = $spreadsheet->getSheet(0); + + $this->assertEquals(29, $firstSheet->getHighestRow()); + $this->assertEquals('N', $firstSheet->getHighestColumn()); + + // Simple cell value + $this->assertEquals('Test String 1', $firstSheet->getCell('A1')->getValue()); + + // Merged cell + $this->assertEquals('BOX', $firstSheet->getCell('B18')->getValue()); + + // Comments/Annotations + $this->assertEquals( + 'Test for a simple colour-formatted string', + $firstSheet->getComment('A1')->getText()->getPlainText() + ); + + // Data types + $this->assertEquals(DataType::TYPE_STRING, $firstSheet->getCell('A1')->getDataType()); + $this->assertEquals(DataType::TYPE_NUMERIC, $firstSheet->getCell('B1')->getDataType()); // Int + + $this->assertEquals(DataType::TYPE_NUMERIC, $firstSheet->getCell('B6')->getDataType()); // Float + $this->assertEquals(1.23, $firstSheet->getCell('B6')->getValue()); + $this->assertEquals(0, $firstSheet->getCell('G10')->getValue()); + + $this->assertEquals(DataType::TYPE_NUMERIC, $firstSheet->getCell('A10')->getDataType()); // Date + $this->assertEquals(22269.0, $firstSheet->getCell('A10')->getValue()); + + $this->assertEquals(DataType::TYPE_NUMERIC, $firstSheet->getCell('A13')->getDataType()); // Time + $this->assertEquals(25569.0625, $firstSheet->getCell('A13')->getValue()); + + $this->assertEquals(DataType::TYPE_NUMERIC, $firstSheet->getCell('A15')->getDataType()); // Date + Time + $this->assertEquals(22269.0625, $firstSheet->getCell('A15')->getValue()); + + $this->assertEquals(DataType::TYPE_NUMERIC, $firstSheet->getCell('A11')->getDataType()); // Fraction + + $this->assertEquals(DataType::TYPE_BOOL, $firstSheet->getCell('D6')->getDataType()); + $this->assertTrue($firstSheet->getCell('D6')->getValue()); + + $this->assertEquals(DataType::TYPE_FORMULA, $firstSheet->getCell('C6')->getDataType()); // Formula + $this->assertEquals('=TRUE()', $firstSheet->getCell('C6')->getValue()); // Formula + + /* + * Percentage, Currency + */ + + $spreadsheet = $this->loadDataFile(); + + $firstSheet = $spreadsheet->getSheet(0); + + $this->assertEquals(DataType::TYPE_NUMERIC, $firstSheet->getCell('A1')->getDataType()); // Percentage (10%) + $this->assertEquals(0.1, $firstSheet->getCell('A1')->getValue()); + + $this->assertEquals(DataType::TYPE_NUMERIC, $firstSheet->getCell('A2')->getDataType()); // Percentage (10.00%) + $this->assertEquals(0.1, $firstSheet->getCell('A2')->getValue()); + + $this->assertEquals(DataType::TYPE_NUMERIC, $firstSheet->getCell('A4')->getDataType()); // Currency (€10.00) + $this->assertEquals(10, $firstSheet->getCell('A4')->getValue()); + + $this->assertEquals(DataType::TYPE_NUMERIC, $firstSheet->getCell('A5')->getDataType()); // Currency ($20) + $this->assertEquals(20, $firstSheet->getCell('A5')->getValue()); + } + + public function testReadColors() + { + $spreadsheet = $this->loadOOCalcTestFile(); + $firstSheet = $spreadsheet->getSheet(0); + + // Background color + + $style = $firstSheet->getCell('K3')->getStyle(); + + $this->assertEquals('none', $style->getFill()->getFillType()); + $this->assertEquals('FFFFFFFF', $style->getFill()->getStartColor()->getARGB()); + $this->assertEquals('FF000000', $style->getFill()->getEndColor()->getARGB()); + } + + public function testReadRichText() + { + $spreadsheet = $this->loadOOCalcTestFile(); + $firstSheet = $spreadsheet->getSheet(0); + + $this->assertEquals( + "I don't know if OOCalc supports Rich Text in the same way as Excel, " . + 'And this row should be autofit height with text wrap', + $firstSheet->getCell('A28')->getValue() + ); + } + + public function testReadCellsWithRepeatedSpaces() + { + $spreadsheet = $this->loadDataFile(); + $firstSheet = $spreadsheet->getSheet(0); + + $this->assertEquals('This has 4 spaces before and 2 after ', $firstSheet->getCell('A8')->getValue()); + $this->assertEquals('This only one after ', $firstSheet->getCell('A9')->getValue()); + $this->assertEquals('Test with DIFFERENT styles and multiple spaces: ', $firstSheet->getCell('A10')->getValue()); + $this->assertEquals("test with new \nLines", $firstSheet->getCell('A11')->getValue()); + } + + public function testReadHyperlinks() + { + $spreadsheet = $this->loadOOCalcTestFile(); + $firstSheet = $spreadsheet->getSheet(0); + + $hyperlink = $firstSheet->getCell('A29'); + + $this->assertEquals(DataType::TYPE_STRING, $hyperlink->getDataType()); + $this->assertEquals('PHPExcel', $hyperlink->getValue()); + $this->assertEquals('http://www.phpexcel.net/', $hyperlink->getHyperlink()->getUrl()); + } + + /* + * Below some test for features not implemented yet + */ + + public function testReadBoldItalicUnderline() + { + $this->markTestSkipped('Features not implemented yet'); + + $spreadsheet = $this->loadOOCalcTestFile(); + $firstSheet = $spreadsheet->getSheet(0); + + // Font styles + + $style = $firstSheet->getCell('A1')->getStyle(); + $this->assertEquals('FF000000', $style->getFont()->getColor()->getARGB()); + $this->assertEquals(11, $style->getFont()->getSize()); + $this->assertEquals(Font::UNDERLINE_NONE, $style->getFont()->getUnderline()); + + $style = $firstSheet->getCell('E3')->getStyle(); + $this->assertEquals(Font::UNDERLINE_SINGLE, $style->getFont()->getUnderline()); + + $style = $firstSheet->getCell('E1')->getStyle(); + $this->assertTrue($style->getFont()->getBold()); + $this->assertTrue($style->getFont()->getItalic()); + } +} diff --git a/tests/data/Reader/Ods/data.ods b/tests/data/Reader/Ods/data.ods new file mode 100644 index 00000000..3171eb6b Binary files /dev/null and b/tests/data/Reader/Ods/data.ods differ