From df9788a6a2837c1c2267dfc98cbaa6f9ad712ae1 Mon Sep 17 00:00:00 2001 From: Paolo Agostinetto Date: Fri, 8 Jan 2016 23:34:20 +0100 Subject: [PATCH 1/3] PHPExcel_Writer_OpenDocument_Content: suppport few basic styles: font color and size, bold, cell background --- .../PHPExcel/Writer/OpenDocument/Content.php | 94 ++++++++++++++++++- 1 file changed, 93 insertions(+), 1 deletion(-) diff --git a/Classes/PHPExcel/Writer/OpenDocument/Content.php b/Classes/PHPExcel/Writer/OpenDocument/Content.php index a34b1670c..e32f0891a 100644 --- a/Classes/PHPExcel/Writer/OpenDocument/Content.php +++ b/Classes/PHPExcel/Writer/OpenDocument/Content.php @@ -33,11 +33,13 @@ * @package PHPExcel_Writer_OpenDocument * @copyright Copyright (c) 2006 - 2015 PHPExcel (http://www.codeplex.com/PHPExcel) * @author Alexander Pervakov + * @author Paolo Agostinetto */ class PHPExcel_Writer_OpenDocument_Content extends PHPExcel_Writer_OpenDocument_WriterPart { const NUMBER_COLS_REPEATED_MAX = 1024; const NUMBER_ROWS_REPEATED_MAX = 1048576; + const CELL_STYLE_PREFIX = 'ce'; /** * Write content.xml to XML format @@ -100,7 +102,11 @@ public function write(PHPExcel $pPHPExcel = null) $objWriter->writeElement('office:scripts'); $objWriter->writeElement('office:font-face-decls'); - $objWriter->writeElement('office:automatic-styles'); + + // Styles XF + $objWriter->startElement('office:automatic-styles'); + $this->writeXfStyles($objWriter, $pPHPExcel); + $objWriter->endElement(); $objWriter->startElement('office:body'); $objWriter->startElement('office:spreadsheet'); @@ -186,12 +192,20 @@ private function writeCells(PHPExcel_Shared_XMLWriter $objWriter, PHPExcel_Works $prev_column = -1; $cells = $row->getCellIterator(); while ($cells->valid()) { + + /** @var PHPExcel_Cell $cell */ $cell = $cells->current(); $column = PHPExcel_Cell::columnIndexFromString($cell->getColumn()) - 1; $this->writeCellSpan($objWriter, $column, $prev_column); $objWriter->startElement('table:table-cell'); + // Style XF + $style = $cell->getXfIndex(); + if($style !== null){ + $objWriter->writeAttribute('table:style-name', self::CELL_STYLE_PREFIX.$style); + } + switch ($cell->getDataType()) { case PHPExcel_Cell_DataType::TYPE_BOOL: $objWriter->writeAttribute('office:value-type', 'boolean'); @@ -269,4 +283,82 @@ private function writeCellSpan(PHPExcel_Shared_XMLWriter $objWriter, $curColumn, $objWriter->endElement(); } } + + /** + * Write XF cell styles + * + * @param PHPExcel_Shared_XMLWriter $objWriter + * @param PHPExcel $pPHPExcel + * @throws PHPExcel_Exception + */ + private function writeXfStyles(PHPExcel_Shared_XMLWriter $objWriter, PHPExcel $pPHPExcel) + { + foreach($pPHPExcel->getCellXfCollection() as $style) { + + $objWriter->startElement('style:style'); + $objWriter->writeAttribute('style:name', self::CELL_STYLE_PREFIX .$style->getIndex()); + $objWriter->writeAttribute('style:family', 'table-cell'); + $objWriter->writeAttribute('style:parent-style-name', 'Default'); + + /* + * style:text-properties + */ + + // Font + $objWriter->startElement('style:text-properties'); + + $font = $style->getFont(); + + if($font->getBold()) { + $objWriter->writeAttribute('fo:font-weight', 'bold'); + $objWriter->writeAttribute('style:font-weight-complex', 'bold'); + $objWriter->writeAttribute('style:font-weight-asian', 'bold'); + } + + if($color = $font->getColor()) { + $objWriter->writeAttribute('fo:color', sprintf('#%s', $color->getRGB())); + } + + if($size = $font->getSize()) { + $objWriter->writeAttribute('fo:font-size', sprintf('%.1fpt', $size)); + } + + $objWriter->endElement(); // Close style:text-properties + + /* + * style:table-cell-properties + */ + + $objWriter->startElement('style:table-cell-properties'); + $objWriter->writeAttribute('style:rotation-align', 'none'); + + // Fill + if($fill = $style->getFill()) { + switch($fill->getFillType()) { + + case \PHPExcel_Style_Fill::FILL_SOLID: + $objWriter->writeAttribute('fo:background-color', sprintf('#%s', + strtolower($fill->getStartColor()->getRGB()) + )); + break; + + case \PHPExcel_Style_Fill::FILL_GRADIENT_LINEAR: + case \PHPExcel_Style_Fill::FILL_GRADIENT_PATH: + /// TODO :: To be implemented + break; + + case \PHPExcel_Style_Fill::FILL_NONE: + default: + } + } + + $objWriter->endElement(); // Close style:table-cell-properties + + /* + * End + */ + + $objWriter->endElement(); // Close style:style + } + } } From 30b078986f658f297bf8d6cb9c39838c007e52bc Mon Sep 17 00:00:00 2001 From: Paolo Agostinetto Date: Sat, 9 Jan 2016 11:59:18 +0100 Subject: [PATCH 2/3] PHPExcel_Writer_OpenDocument_Content: suppport italic, single underline --- Classes/PHPExcel/Writer/OpenDocument/Content.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Classes/PHPExcel/Writer/OpenDocument/Content.php b/Classes/PHPExcel/Writer/OpenDocument/Content.php index e32f0891a..3f8384df6 100644 --- a/Classes/PHPExcel/Writer/OpenDocument/Content.php +++ b/Classes/PHPExcel/Writer/OpenDocument/Content.php @@ -315,6 +315,10 @@ private function writeXfStyles(PHPExcel_Shared_XMLWriter $objWriter, PHPExcel $p $objWriter->writeAttribute('style:font-weight-asian', 'bold'); } + if($font->getItalic()) { + $objWriter->writeAttribute('fo:font-style', 'italic'); + } + if($color = $font->getColor()) { $objWriter->writeAttribute('fo:color', sprintf('#%s', $color->getRGB())); } @@ -323,6 +327,12 @@ private function writeXfStyles(PHPExcel_Shared_XMLWriter $objWriter, PHPExcel $p $objWriter->writeAttribute('fo:font-size', sprintf('%.1fpt', $size)); } + if($font->getUnderline() == \PHPExcel_Style_Font::UNDERLINE_SINGLE) { + $objWriter->writeAttribute('style:text-underline-style', 'solid'); + $objWriter->writeAttribute('style:text-underline-width', 'auto'); + $objWriter->writeAttribute('style:text-underline-color', 'font-color'); + } + $objWriter->endElement(); // Close style:text-properties /* From a588c611bc5c51ab2dfa0019d124671ca3bc592e Mon Sep 17 00:00:00 2001 From: Paolo Agostinetto Date: Tue, 7 Feb 2017 11:36:49 +0100 Subject: [PATCH 3/3] OOCalc reader: read correctly multiple spaces --- .gitignore | 1 + Classes/PHPExcel/Reader/OOCalc.php | 221 ++++++++++++------ .../Classes/PHPExcel/Reader/OOCalcTest.php | 26 +++ .../Reader/OOCalc/spaces-everywhere.ods | Bin 0 -> 10101 bytes 4 files changed, 171 insertions(+), 77 deletions(-) create mode 100644 unitTests/Classes/PHPExcel/Reader/OOCalcTest.php create mode 100644 unitTests/rawTestData/Reader/OOCalc/spaces-everywhere.ods diff --git a/.gitignore b/.gitignore index dea03b5e1..f76f37bd2 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ analysis *.project /.settings /.idea +/unitTests/phpunit diff --git a/Classes/PHPExcel/Reader/OOCalc.php b/Classes/PHPExcel/Reader/OOCalc.php index a889d9570..5251df6f4 100644 --- a/Classes/PHPExcel/Reader/OOCalc.php +++ b/Classes/PHPExcel/Reader/OOCalc.php @@ -34,34 +34,31 @@ * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt LGPL * @version ##VERSION##, ##DATE## */ -class PHPExcel_Reader_OOCalc extends PHPExcel_Reader_Abstract implements PHPExcel_Reader_IReader -{ +class PHPExcel_Reader_OOCalc extends PHPExcel_Reader_Abstract implements PHPExcel_Reader_IReader { /** * Formats * * @var array */ - private $styles = array(); + private $styles = []; /** * Create a new PHPExcel_Reader_OOCalc */ - public function __construct() - { - $this->readFilter = new PHPExcel_Reader_DefaultReadFilter(); + public function __construct(){ + $this->readFilter = new PHPExcel_Reader_DefaultReadFilter(); } /** * Can the current PHPExcel_Reader_IReader read the file? * - * @param string $pFilename + * @param string $pFilename * @return boolean * @throws PHPExcel_Reader_Exception */ - public function canRead($pFilename) - { + public function canRead($pFilename){ // Check if file exists - if (!file_exists($pFilename)) { + if(!file_exists($pFilename)){ throw new PHPExcel_Reader_Exception("Could not open " . $pFilename . " for reading! File does not exist."); } @@ -75,20 +72,21 @@ public function canRead($pFilename) $mimeType = 'UNKNOWN'; // Load file $zip = new $zipClass; - if ($zip->open($pFilename) === true) { + if($zip->open($pFilename) === true){ // check if it is an OOXML archive $stat = $zip->statName('mimetype'); - if ($stat && ($stat['size'] <= 255)) { + if($stat && ($stat['size'] <= 255)){ $mimeType = $zip->getFromName($stat['name']); - } elseif ($stat = $zip->statName('META-INF/manifest.xml')) { + } + elseif($stat = $zip->statName('META-INF/manifest.xml')){ $xml = simplexml_load_string($this->securityScan($zip->getFromName('META-INF/manifest.xml')), 'SimpleXMLElement', PHPExcel_Settings::getLibXmlLoaderOptions()); $namespacesContent = $xml->getNamespaces(true); - if (isset($namespacesContent['manifest'])) { + if(isset($namespacesContent['manifest'])){ $manifest = $xml->children($namespacesContent['manifest']); - foreach ($manifest as $manifestDataSet) { + foreach($manifest as $manifestDataSet){ $manifestAttributes = $manifestDataSet->attributes($namespacesContent['manifest']); - if ($manifestAttributes->{'full-path'} == '/') { - $mimeType = (string) $manifestAttributes->{'media-type'}; + if($manifestAttributes->{'full-path'} == '/'){ + $mimeType = (string)$manifestAttributes->{'media-type'}; break; } } @@ -107,48 +105,48 @@ public function canRead($pFilename) /** * Reads names of the worksheets from a file, without parsing the whole file to a PHPExcel object * - * @param string $pFilename + * @param string $pFilename * @throws PHPExcel_Reader_Exception */ - public function listWorksheetNames($pFilename) - { + public function listWorksheetNames($pFilename){ // Check if file exists - if (!file_exists($pFilename)) { + if(!file_exists($pFilename)){ throw new PHPExcel_Reader_Exception("Could not open " . $pFilename . " for reading! File does not exist."); } $zipClass = PHPExcel_Settings::getZipClass(); $zip = new $zipClass; - if (!$zip->open($pFilename)) { + if(!$zip->open($pFilename)){ throw new PHPExcel_Reader_Exception("Could not open " . $pFilename . " for reading! Error opening file."); } - $worksheetNames = array(); + $worksheetNames = []; $xml = new XMLReader(); - $res = $xml->xml($this->securityScanFile('zip://'.realpath($pFilename).'#content.xml'), null, PHPExcel_Settings::getLibXmlLoaderOptions()); + $res = $xml->xml($this->securityScanFile('zip://' . realpath($pFilename) . '#content.xml'), null, PHPExcel_Settings::getLibXmlLoaderOptions()); $xml->setParserProperty(2, true); // Step into the first level of content of the XML $xml->read(); - while ($xml->read()) { + while($xml->read()){ // Quickly jump through to the office:body node - while ($xml->name !== 'office:body') { - if ($xml->isEmptyElement) { + while($xml->name !== 'office:body'){ + if($xml->isEmptyElement){ $xml->read(); - } else { + } + else{ $xml->next(); } } // Now read each node until we find our first table:table node - while ($xml->read()) { - if ($xml->name == 'table:table' && $xml->nodeType == XMLReader::ELEMENT) { + 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 - do { + 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); } } } @@ -159,58 +157,58 @@ public function listWorksheetNames($pFilename) /** * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns) * - * @param string $pFilename + * @param string $pFilename * @throws PHPExcel_Reader_Exception */ - public function listWorksheetInfo($pFilename) - { + public function listWorksheetInfo($pFilename){ // Check if file exists - if (!file_exists($pFilename)) { + if(!file_exists($pFilename)){ throw new PHPExcel_Reader_Exception("Could not open " . $pFilename . " for reading! File does not exist."); } - $worksheetInfo = array(); + $worksheetInfo = []; $zipClass = PHPExcel_Settings::getZipClass(); $zip = new $zipClass; - if (!$zip->open($pFilename)) { + if(!$zip->open($pFilename)){ throw new PHPExcel_Reader_Exception("Could not open " . $pFilename . " for reading! Error opening file."); } $xml = new XMLReader(); - $res = $xml->xml($this->securityScanFile('zip://'.realpath($pFilename).'#content.xml'), null, PHPExcel_Settings::getLibXmlLoaderOptions()); + $res = $xml->xml($this->securityScanFile('zip://' . realpath($pFilename) . '#content.xml'), null, PHPExcel_Settings::getLibXmlLoaderOptions()); $xml->setParserProperty(2, true); // Step into the first level of content of the XML $xml->read(); - while ($xml->read()) { + while($xml->read()){ // Quickly jump through to the office:body node - while ($xml->name !== 'office:body') { - if ($xml->isEmptyElement) { + while($xml->name !== 'office:body'){ + if($xml->isEmptyElement){ $xml->read(); - } else { + } + else{ $xml->next(); } } - // Now read each node until we find our first table:table node - while ($xml->read()) { - if ($xml->name == 'table:table' && $xml->nodeType == XMLReader::ELEMENT) { + // Now read each node until we find our first table:table node + while($xml->read()){ + if($xml->name == 'table:table' && $xml->nodeType == XMLReader::ELEMENT){ $worksheetNames[] = $xml->getAttribute('table:name'); - $tmpInfo = array( + $tmpInfo = [ 'worksheetName' => $xml->getAttribute('table:name'), 'lastColumnLetter' => 'A', 'lastColumnIndex' => 0, 'totalRows' => 0, 'totalColumns' => 0, - ); + ]; // Loop through each child node of the table:table element reading $currCells = 0; - do { + 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; @@ -218,22 +216,24 @@ public function listWorksheetInfo($pFilename) $currCells = 0; // Step into the row $xml->read(); - do { - if ($xml->name == 'table:table-cell' && $xml->nodeType == XMLReader::ELEMENT) { - if (!$xml->isEmptyElement) { + do{ + if($xml->name == 'table:table-cell' && $xml->nodeType == XMLReader::ELEMENT){ + if(!$xml->isEmptyElement){ $currCells++; $xml->next(); - } else { + } + 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(); } - } while ($xml->name != 'table:table-row'); + }while($xml->name != 'table:table-row'); } - } while ($xml->name != 'table:table'); + }while($xml->name != 'table:table'); $tmpInfo['totalColumns'] = max($tmpInfo['totalColumns'], $currCells); $tmpInfo['lastColumnIndex'] = $tmpInfo['totalColumns'] - 1; @@ -284,12 +284,11 @@ public function listWorksheetInfo($pFilename) /** * Loads PHPExcel from file * - * @param string $pFilename + * @param string $pFilename * @return PHPExcel * @throws PHPExcel_Reader_Exception */ - public function load($pFilename) - { + public function load($pFilename){ // Create new PHPExcel $objPHPExcel = new PHPExcel(); @@ -297,29 +296,31 @@ public function load($pFilename) return $this->loadIntoExisting($pFilename, $objPHPExcel); } - private static function identifyFixedStyleValue($styleList, &$styleAttributeValue) - { + private static function identifyFixedStyleValue($styleList, &$styleAttributeValue){ $styleAttributeValue = strtolower($styleAttributeValue); - foreach ($styleList as $style) { - if ($styleAttributeValue == strtolower($style)) { + foreach($styleList as $style){ + if($styleAttributeValue == strtolower($style)){ $styleAttributeValue = $style; + return true; } } + return false; } /** * Loads PHPExcel from file into PHPExcel instance * - * @param string $pFilename - * @param PHPExcel $objPHPExcel + * @param string $pFilename + * @param PHPExcel $objPHPExcel * @return PHPExcel * @throws PHPExcel_Reader_Exception */ public function loadIntoExisting($pFilename, PHPExcel $objPHPExcel) { - // Check if file exists + +// Check if file exists if (!file_exists($pFilename)) { throw new PHPExcel_Reader_Exception("Could not open " . $pFilename . " for reading! File does not exist."); } @@ -329,6 +330,7 @@ public function loadIntoExisting($pFilename, PHPExcel $objPHPExcel) $zipClass = PHPExcel_Settings::getZipClass(); + /** @var \ZipArchive $zip */ $zip = new $zipClass; if (!$zip->open($pFilename)) { throw new PHPExcel_Reader_Exception("Could not open " . $pFilename . " for reading! Error opening file."); @@ -429,9 +431,13 @@ public function loadIntoExisting($pFilename, PHPExcel $objPHPExcel) $workbook = $xml->children($namespacesContent['office']); foreach ($workbook->body->spreadsheet as $workbookData) { + /** @var \SimpleXMLElement $workbookData */ + $workbookData = $workbookData->children($namespacesContent['table']); $worksheetID = 0; foreach ($workbookData->table as $worksheetDataSet) { + /** @var \SimpleXMLElement $worksheetDataSet */ + $worksheetData = $worksheetDataSet->children($namespacesContent['table']); // print_r($worksheetData); // echo '
'; @@ -457,6 +463,8 @@ public function loadIntoExisting($pFilename, PHPExcel $objPHPExcel) $rowID = 1; foreach ($worksheetData as $key => $rowData) { + /** @var \SimpleXMLElement $rowData */ + // echo ''.$key.'
'; switch ($key) { case 'table-header-rows': @@ -469,6 +477,8 @@ public function loadIntoExisting($pFilename, PHPExcel $objPHPExcel) $rowRepeats = (isset($rowDataTableAttributes['number-rows-repeated'])) ? $rowDataTableAttributes['number-rows-repeated'] : 1; $columnID = 'A'; foreach ($rowData as $key => $cellData) { + /** @var \SimpleXMLElement $cellData */ + if ($this->getReadFilter() !== null) { if (!$this->getReadFilter()->readCell($columnID, $rowID, $worksheetName)) { continue; @@ -524,16 +534,33 @@ public function loadIntoExisting($pFilename, PHPExcel $objPHPExcel) // 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); - } else { - array_push($dataArray, $pData); - } + /** @var \SimpleXMLElement $pData */ + +// if (isset($pData->span)) { +// // span sections do not newline, so we just create one large string here +// $spanSection = ""; +// foreach ($pData->span as $spanData) { +// /** @var \SimpleXMLElement $spanData */ +// +// $spanSection .= $spanData; +// } +// array_push($dataArray, $spanSection); +// } else { + + // SimpleXML sucks, need to use DOMDocument instead + libxml_use_internal_errors(true); + + $doc = new \DOMDocument("1.0"); + $doc->loadXML($pData->saveXML()); + + $str = $this->scanElement($doc->childNodes->item(0)); + + unset($doc); + + libxml_use_internal_errors(false); + + array_push($dataArray, $str); +// } } $allCellDataText = implode($dataArray, "\n"); @@ -685,6 +712,46 @@ public function loadIntoExisting($pFilename, PHPExcel $objPHPExcel) return $objPHPExcel; } + /** + * Recursively scan element + * + * @param DOMNode $element + * @return string + */ + protected function scanElement(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->scanElement($child); + } + } + + return $str; + } + private function parseRichText($is = '') { $value = new PHPExcel_RichText(); diff --git a/unitTests/Classes/PHPExcel/Reader/OOCalcTest.php b/unitTests/Classes/PHPExcel/Reader/OOCalcTest.php new file mode 100644 index 000000000..16f0df089 --- /dev/null +++ b/unitTests/Classes/PHPExcel/Reader/OOCalcTest.php @@ -0,0 +1,26 @@ +load(__DIR__."/../../../rawTestData/Reader/OOCalc/spaces-everywhere.ods"); + + $arr = $file->getActiveSheet()->toArray(); + + $this->assertEquals([ + ["This has 4 spaces before and 2 after "], + ["This only one after "], + ["Test with DIFFERENT styles and multiple spaces: "], + ["test with new \nLines"], + ], $arr); + } +} diff --git a/unitTests/rawTestData/Reader/OOCalc/spaces-everywhere.ods b/unitTests/rawTestData/Reader/OOCalc/spaces-everywhere.ods new file mode 100644 index 0000000000000000000000000000000000000000..2c0867252fd7abf8c4ab360cca539fc6eec50230 GIT binary patch literal 10101 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 HL+`%;l=3A@ literal 0 HcmV?d00001