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