1<?php 2 3namespace PhpOffice\PhpSpreadsheet\Writer\Ods; 4 5use PhpOffice\PhpSpreadsheet\Cell\Cell; 6use PhpOffice\PhpSpreadsheet\Cell\Coordinate; 7use PhpOffice\PhpSpreadsheet\Cell\DataType; 8use PhpOffice\PhpSpreadsheet\Shared\XMLWriter; 9use PhpOffice\PhpSpreadsheet\Spreadsheet; 10use PhpOffice\PhpSpreadsheet\Worksheet\Row; 11use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; 12use PhpOffice\PhpSpreadsheet\Writer\Exception; 13use PhpOffice\PhpSpreadsheet\Writer\Ods; 14use PhpOffice\PhpSpreadsheet\Writer\Ods\Cell\Comment; 15use PhpOffice\PhpSpreadsheet\Writer\Ods\Cell\Style; 16 17/** 18 * @author Alexander Pervakov <frost-nzcr4@jagmort.com> 19 */ 20class Content extends WriterPart 21{ 22 const NUMBER_COLS_REPEATED_MAX = 1024; 23 const NUMBER_ROWS_REPEATED_MAX = 1048576; 24 25 private $formulaConvertor; 26 27 /** 28 * Set parent Ods writer. 29 */ 30 public function __construct(Ods $writer) 31 { 32 parent::__construct($writer); 33 34 $this->formulaConvertor = new Formula($this->getParentWriter()->getSpreadsheet()->getDefinedNames()); 35 } 36 37 /** 38 * Write content.xml to XML format. 39 * 40 * @return string XML Output 41 */ 42 public function write(): string 43 { 44 $objWriter = null; 45 if ($this->getParentWriter()->getUseDiskCaching()) { 46 $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory()); 47 } else { 48 $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY); 49 } 50 51 // XML header 52 $objWriter->startDocument('1.0', 'UTF-8'); 53 54 // Content 55 $objWriter->startElement('office:document-content'); 56 $objWriter->writeAttribute('xmlns:office', 'urn:oasis:names:tc:opendocument:xmlns:office:1.0'); 57 $objWriter->writeAttribute('xmlns:style', 'urn:oasis:names:tc:opendocument:xmlns:style:1.0'); 58 $objWriter->writeAttribute('xmlns:text', 'urn:oasis:names:tc:opendocument:xmlns:text:1.0'); 59 $objWriter->writeAttribute('xmlns:table', 'urn:oasis:names:tc:opendocument:xmlns:table:1.0'); 60 $objWriter->writeAttribute('xmlns:draw', 'urn:oasis:names:tc:opendocument:xmlns:drawing:1.0'); 61 $objWriter->writeAttribute('xmlns:fo', 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0'); 62 $objWriter->writeAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink'); 63 $objWriter->writeAttribute('xmlns:dc', 'http://purl.org/dc/elements/1.1/'); 64 $objWriter->writeAttribute('xmlns:meta', 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0'); 65 $objWriter->writeAttribute('xmlns:number', 'urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0'); 66 $objWriter->writeAttribute('xmlns:presentation', 'urn:oasis:names:tc:opendocument:xmlns:presentation:1.0'); 67 $objWriter->writeAttribute('xmlns:svg', 'urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0'); 68 $objWriter->writeAttribute('xmlns:chart', 'urn:oasis:names:tc:opendocument:xmlns:chart:1.0'); 69 $objWriter->writeAttribute('xmlns:dr3d', 'urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0'); 70 $objWriter->writeAttribute('xmlns:math', 'http://www.w3.org/1998/Math/MathML'); 71 $objWriter->writeAttribute('xmlns:form', 'urn:oasis:names:tc:opendocument:xmlns:form:1.0'); 72 $objWriter->writeAttribute('xmlns:script', 'urn:oasis:names:tc:opendocument:xmlns:script:1.0'); 73 $objWriter->writeAttribute('xmlns:ooo', 'http://openoffice.org/2004/office'); 74 $objWriter->writeAttribute('xmlns:ooow', 'http://openoffice.org/2004/writer'); 75 $objWriter->writeAttribute('xmlns:oooc', 'http://openoffice.org/2004/calc'); 76 $objWriter->writeAttribute('xmlns:dom', 'http://www.w3.org/2001/xml-events'); 77 $objWriter->writeAttribute('xmlns:xforms', 'http://www.w3.org/2002/xforms'); 78 $objWriter->writeAttribute('xmlns:xsd', 'http://www.w3.org/2001/XMLSchema'); 79 $objWriter->writeAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance'); 80 $objWriter->writeAttribute('xmlns:rpt', 'http://openoffice.org/2005/report'); 81 $objWriter->writeAttribute('xmlns:of', 'urn:oasis:names:tc:opendocument:xmlns:of:1.2'); 82 $objWriter->writeAttribute('xmlns:xhtml', 'http://www.w3.org/1999/xhtml'); 83 $objWriter->writeAttribute('xmlns:grddl', 'http://www.w3.org/2003/g/data-view#'); 84 $objWriter->writeAttribute('xmlns:tableooo', 'http://openoffice.org/2009/table'); 85 $objWriter->writeAttribute('xmlns:field', 'urn:openoffice:names:experimental:ooo-ms-interop:xmlns:field:1.0'); 86 $objWriter->writeAttribute('xmlns:formx', 'urn:openoffice:names:experimental:ooxml-odf-interop:xmlns:form:1.0'); 87 $objWriter->writeAttribute('xmlns:css3t', 'http://www.w3.org/TR/css3-text/'); 88 $objWriter->writeAttribute('office:version', '1.2'); 89 90 $objWriter->writeElement('office:scripts'); 91 $objWriter->writeElement('office:font-face-decls'); 92 93 // Styles XF 94 $objWriter->startElement('office:automatic-styles'); 95 $this->writeXfStyles($objWriter, $this->getParentWriter()->getSpreadsheet()); 96 $objWriter->endElement(); 97 98 $objWriter->startElement('office:body'); 99 $objWriter->startElement('office:spreadsheet'); 100 $objWriter->writeElement('table:calculation-settings'); 101 102 $this->writeSheets($objWriter); 103 104 (new AutoFilters($objWriter, $this->getParentWriter()->getSpreadsheet()))->write(); 105 // Defined names (ranges and formulae) 106 (new NamedExpressions($objWriter, $this->getParentWriter()->getSpreadsheet(), $this->formulaConvertor))->write(); 107 108 $objWriter->endElement(); 109 $objWriter->endElement(); 110 $objWriter->endElement(); 111 112 return $objWriter->getData(); 113 } 114 115 /** 116 * Write sheets. 117 */ 118 private function writeSheets(XMLWriter $objWriter): void 119 { 120 $spreadsheet = $this->getParentWriter()->getSpreadsheet(); /** @var Spreadsheet $spreadsheet */ 121 $sheetCount = $spreadsheet->getSheetCount(); 122 for ($i = 0; $i < $sheetCount; ++$i) { 123 $objWriter->startElement('table:table'); 124 $objWriter->writeAttribute('table:name', $spreadsheet->getSheet($i)->getTitle()); 125 $objWriter->writeElement('office:forms'); 126 $objWriter->startElement('table:table-column'); 127 $objWriter->writeAttribute('table:number-columns-repeated', self::NUMBER_COLS_REPEATED_MAX); 128 $objWriter->endElement(); 129 $this->writeRows($objWriter, $spreadsheet->getSheet($i)); 130 $objWriter->endElement(); 131 } 132 } 133 134 /** 135 * Write rows of the specified sheet. 136 */ 137 private function writeRows(XMLWriter $objWriter, Worksheet $sheet): void 138 { 139 $numberRowsRepeated = self::NUMBER_ROWS_REPEATED_MAX; 140 $span_row = 0; 141 $rows = $sheet->getRowIterator(); 142 while ($rows->valid()) { 143 --$numberRowsRepeated; 144 $row = $rows->current(); 145 if ($row->getCellIterator()->valid()) { 146 if ($span_row) { 147 $objWriter->startElement('table:table-row'); 148 if ($span_row > 1) { 149 $objWriter->writeAttribute('table:number-rows-repeated', $span_row); 150 } 151 $objWriter->startElement('table:table-cell'); 152 $objWriter->writeAttribute('table:number-columns-repeated', self::NUMBER_COLS_REPEATED_MAX); 153 $objWriter->endElement(); 154 $objWriter->endElement(); 155 $span_row = 0; 156 } 157 $objWriter->startElement('table:table-row'); 158 $this->writeCells($objWriter, $row); 159 $objWriter->endElement(); 160 } else { 161 ++$span_row; 162 } 163 $rows->next(); 164 } 165 } 166 167 /** 168 * Write cells of the specified row. 169 */ 170 private function writeCells(XMLWriter $objWriter, Row $row): void 171 { 172 $numberColsRepeated = self::NUMBER_COLS_REPEATED_MAX; 173 $prevColumn = -1; 174 $cells = $row->getCellIterator(); 175 while ($cells->valid()) { 176 /** @var \PhpOffice\PhpSpreadsheet\Cell\Cell $cell */ 177 $cell = $cells->current(); 178 $column = Coordinate::columnIndexFromString($cell->getColumn()) - 1; 179 180 $this->writeCellSpan($objWriter, $column, $prevColumn); 181 $objWriter->startElement('table:table-cell'); 182 $this->writeCellMerge($objWriter, $cell); 183 184 // Style XF 185 $style = $cell->getXfIndex(); 186 if ($style !== null) { 187 $objWriter->writeAttribute('table:style-name', Style::CELL_STYLE_PREFIX . $style); 188 } 189 190 switch ($cell->getDataType()) { 191 case DataType::TYPE_BOOL: 192 $objWriter->writeAttribute('office:value-type', 'boolean'); 193 $objWriter->writeAttribute('office:value', $cell->getValue()); 194 $objWriter->writeElement('text:p', $cell->getValue()); 195 196 break; 197 case DataType::TYPE_ERROR: 198 $objWriter->writeAttribute('table:formula', 'of:=#NULL!'); 199 $objWriter->writeAttribute('office:value-type', 'string'); 200 $objWriter->writeAttribute('office:string-value', ''); 201 $objWriter->writeElement('text:p', '#NULL!'); 202 203 break; 204 case DataType::TYPE_FORMULA: 205 $formulaValue = $cell->getValue(); 206 if ($this->getParentWriter()->getPreCalculateFormulas()) { 207 try { 208 $formulaValue = $cell->getCalculatedValue(); 209 } catch (Exception $e) { 210 // don't do anything 211 } 212 } 213 $objWriter->writeAttribute('table:formula', $this->formulaConvertor->convertFormula($cell->getValue())); 214 if (is_numeric($formulaValue)) { 215 $objWriter->writeAttribute('office:value-type', 'float'); 216 } else { 217 $objWriter->writeAttribute('office:value-type', 'string'); 218 } 219 $objWriter->writeAttribute('office:value', $formulaValue); 220 $objWriter->writeElement('text:p', $formulaValue); 221 222 break; 223 case DataType::TYPE_NUMERIC: 224 $objWriter->writeAttribute('office:value-type', 'float'); 225 $objWriter->writeAttribute('office:value', $cell->getValue()); 226 $objWriter->writeElement('text:p', $cell->getValue()); 227 228 break; 229 case DataType::TYPE_INLINE: 230 // break intentionally omitted 231 case DataType::TYPE_STRING: 232 $objWriter->writeAttribute('office:value-type', 'string'); 233 $objWriter->writeElement('text:p', $cell->getValue()); 234 235 break; 236 } 237 Comment::write($objWriter, $cell); 238 $objWriter->endElement(); 239 $prevColumn = $column; 240 $cells->next(); 241 } 242 $numberColsRepeated = $numberColsRepeated - $prevColumn - 1; 243 if ($numberColsRepeated > 0) { 244 if ($numberColsRepeated > 1) { 245 $objWriter->startElement('table:table-cell'); 246 $objWriter->writeAttribute('table:number-columns-repeated', $numberColsRepeated); 247 $objWriter->endElement(); 248 } else { 249 $objWriter->writeElement('table:table-cell'); 250 } 251 } 252 } 253 254 /** 255 * Write span. 256 * 257 * @param int $curColumn 258 * @param int $prevColumn 259 */ 260 private function writeCellSpan(XMLWriter $objWriter, $curColumn, $prevColumn): void 261 { 262 $diff = $curColumn - $prevColumn - 1; 263 if (1 === $diff) { 264 $objWriter->writeElement('table:table-cell'); 265 } elseif ($diff > 1) { 266 $objWriter->startElement('table:table-cell'); 267 $objWriter->writeAttribute('table:number-columns-repeated', $diff); 268 $objWriter->endElement(); 269 } 270 } 271 272 /** 273 * Write XF cell styles. 274 */ 275 private function writeXfStyles(XMLWriter $writer, Spreadsheet $spreadsheet): void 276 { 277 $styleWriter = new Style($writer); 278 foreach ($spreadsheet->getCellXfCollection() as $style) { 279 $styleWriter->write($style); 280 } 281 } 282 283 /** 284 * Write attributes for merged cell. 285 */ 286 private function writeCellMerge(XMLWriter $objWriter, Cell $cell): void 287 { 288 if (!$cell->isMergeRangeValueCell()) { 289 return; 290 } 291 292 $mergeRange = Coordinate::splitRange($cell->getMergeRange()); 293 [$startCell, $endCell] = $mergeRange[0]; 294 $start = Coordinate::coordinateFromString($startCell); 295 $end = Coordinate::coordinateFromString($endCell); 296 $columnSpan = Coordinate::columnIndexFromString($end[0]) - Coordinate::columnIndexFromString($start[0]) + 1; 297 $rowSpan = ((int) $end[1]) - ((int) $start[1]) + 1; 298 299 $objWriter->writeAttribute('table:number-columns-spanned', $columnSpan); 300 $objWriter->writeAttribute('table:number-rows-spanned', $rowSpan); 301 } 302} 303