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\Style\Fill;
11use PhpOffice\PhpSpreadsheet\Style\Font;
12use PhpOffice\PhpSpreadsheet\Worksheet\Row;
13use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
14use PhpOffice\PhpSpreadsheet\Writer\Exception;
15use PhpOffice\PhpSpreadsheet\Writer\Ods;
16use PhpOffice\PhpSpreadsheet\Writer\Ods\Cell\Comment;
17
18/**
19 * @author     Alexander Pervakov <frost-nzcr4@jagmort.com>
20 */
21class Content extends WriterPart
22{
23    const NUMBER_COLS_REPEATED_MAX = 1024;
24    const NUMBER_ROWS_REPEATED_MAX = 1048576;
25    const CELL_STYLE_PREFIX = 'ce';
26
27    private $formulaConvertor;
28
29    /**
30     * Set parent Ods writer.
31     */
32    public function __construct(Ods $writer)
33    {
34        parent::__construct($writer);
35
36        $this->formulaConvertor = new Formula($this->getParentWriter()->getSpreadsheet()->getDefinedNames());
37    }
38
39    /**
40     * Write content.xml to XML format.
41     *
42     * @return string XML Output
43     */
44    public function write()
45    {
46        $objWriter = null;
47        if ($this->getParentWriter()->getUseDiskCaching()) {
48            $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory());
49        } else {
50            $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY);
51        }
52
53        // XML header
54        $objWriter->startDocument('1.0', 'UTF-8');
55
56        // Content
57        $objWriter->startElement('office:document-content');
58        $objWriter->writeAttribute('xmlns:office', 'urn:oasis:names:tc:opendocument:xmlns:office:1.0');
59        $objWriter->writeAttribute('xmlns:style', 'urn:oasis:names:tc:opendocument:xmlns:style:1.0');
60        $objWriter->writeAttribute('xmlns:text', 'urn:oasis:names:tc:opendocument:xmlns:text:1.0');
61        $objWriter->writeAttribute('xmlns:table', 'urn:oasis:names:tc:opendocument:xmlns:table:1.0');
62        $objWriter->writeAttribute('xmlns:draw', 'urn:oasis:names:tc:opendocument:xmlns:drawing:1.0');
63        $objWriter->writeAttribute('xmlns:fo', 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0');
64        $objWriter->writeAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
65        $objWriter->writeAttribute('xmlns:dc', 'http://purl.org/dc/elements/1.1/');
66        $objWriter->writeAttribute('xmlns:meta', 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0');
67        $objWriter->writeAttribute('xmlns:number', 'urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0');
68        $objWriter->writeAttribute('xmlns:presentation', 'urn:oasis:names:tc:opendocument:xmlns:presentation:1.0');
69        $objWriter->writeAttribute('xmlns:svg', 'urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0');
70        $objWriter->writeAttribute('xmlns:chart', 'urn:oasis:names:tc:opendocument:xmlns:chart:1.0');
71        $objWriter->writeAttribute('xmlns:dr3d', 'urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0');
72        $objWriter->writeAttribute('xmlns:math', 'http://www.w3.org/1998/Math/MathML');
73        $objWriter->writeAttribute('xmlns:form', 'urn:oasis:names:tc:opendocument:xmlns:form:1.0');
74        $objWriter->writeAttribute('xmlns:script', 'urn:oasis:names:tc:opendocument:xmlns:script:1.0');
75        $objWriter->writeAttribute('xmlns:ooo', 'http://openoffice.org/2004/office');
76        $objWriter->writeAttribute('xmlns:ooow', 'http://openoffice.org/2004/writer');
77        $objWriter->writeAttribute('xmlns:oooc', 'http://openoffice.org/2004/calc');
78        $objWriter->writeAttribute('xmlns:dom', 'http://www.w3.org/2001/xml-events');
79        $objWriter->writeAttribute('xmlns:xforms', 'http://www.w3.org/2002/xforms');
80        $objWriter->writeAttribute('xmlns:xsd', 'http://www.w3.org/2001/XMLSchema');
81        $objWriter->writeAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance');
82        $objWriter->writeAttribute('xmlns:rpt', 'http://openoffice.org/2005/report');
83        $objWriter->writeAttribute('xmlns:of', 'urn:oasis:names:tc:opendocument:xmlns:of:1.2');
84        $objWriter->writeAttribute('xmlns:xhtml', 'http://www.w3.org/1999/xhtml');
85        $objWriter->writeAttribute('xmlns:grddl', 'http://www.w3.org/2003/g/data-view#');
86        $objWriter->writeAttribute('xmlns:tableooo', 'http://openoffice.org/2009/table');
87        $objWriter->writeAttribute('xmlns:field', 'urn:openoffice:names:experimental:ooo-ms-interop:xmlns:field:1.0');
88        $objWriter->writeAttribute('xmlns:formx', 'urn:openoffice:names:experimental:ooxml-odf-interop:xmlns:form:1.0');
89        $objWriter->writeAttribute('xmlns:css3t', 'http://www.w3.org/TR/css3-text/');
90        $objWriter->writeAttribute('office:version', '1.2');
91
92        $objWriter->writeElement('office:scripts');
93        $objWriter->writeElement('office:font-face-decls');
94
95        // Styles XF
96        $objWriter->startElement('office:automatic-styles');
97        $this->writeXfStyles($objWriter, $this->getParentWriter()->getSpreadsheet());
98        $objWriter->endElement();
99
100        $objWriter->startElement('office:body');
101        $objWriter->startElement('office:spreadsheet');
102        $objWriter->writeElement('table:calculation-settings');
103
104        $this->writeSheets($objWriter);
105
106        // Defined names (ranges and formulae)
107        (new NamedExpressions($objWriter, $this->getParentWriter()->getSpreadsheet(), $this->formulaConvertor))->write();
108
109        $objWriter->endElement();
110        $objWriter->endElement();
111        $objWriter->endElement();
112
113        return $objWriter->getData();
114    }
115
116    /**
117     * Write sheets.
118     */
119    private function writeSheets(XMLWriter $objWriter): void
120    {
121        $spreadsheet = $this->getParentWriter()->getSpreadsheet(); /** @var Spreadsheet $spreadsheet */
122        $sheetCount = $spreadsheet->getSheetCount();
123        for ($i = 0; $i < $sheetCount; ++$i) {
124            $objWriter->startElement('table:table');
125            $objWriter->writeAttribute('table:name', $spreadsheet->getSheet($i)->getTitle());
126            $objWriter->writeElement('office:forms');
127            $objWriter->startElement('table:table-column');
128            $objWriter->writeAttribute('table:number-columns-repeated', self::NUMBER_COLS_REPEATED_MAX);
129            $objWriter->endElement();
130            $this->writeRows($objWriter, $spreadsheet->getSheet($i));
131            $objWriter->endElement();
132        }
133    }
134
135    /**
136     * Write rows of the specified sheet.
137     */
138    private function writeRows(XMLWriter $objWriter, Worksheet $sheet): void
139    {
140        $numberRowsRepeated = self::NUMBER_ROWS_REPEATED_MAX;
141        $span_row = 0;
142        $rows = $sheet->getRowIterator();
143        while ($rows->valid()) {
144            --$numberRowsRepeated;
145            $row = $rows->current();
146            if ($row->getCellIterator()->valid()) {
147                if ($span_row) {
148                    $objWriter->startElement('table:table-row');
149                    if ($span_row > 1) {
150                        $objWriter->writeAttribute('table:number-rows-repeated', $span_row);
151                    }
152                    $objWriter->startElement('table:table-cell');
153                    $objWriter->writeAttribute('table:number-columns-repeated', self::NUMBER_COLS_REPEATED_MAX);
154                    $objWriter->endElement();
155                    $objWriter->endElement();
156                    $span_row = 0;
157                }
158                $objWriter->startElement('table:table-row');
159                $this->writeCells($objWriter, $row);
160                $objWriter->endElement();
161            } else {
162                ++$span_row;
163            }
164            $rows->next();
165        }
166    }
167
168    /**
169     * Write cells of the specified row.
170     */
171    private function writeCells(XMLWriter $objWriter, Row $row): void
172    {
173        $numberColsRepeated = self::NUMBER_COLS_REPEATED_MAX;
174        $prevColumn = -1;
175        $cells = $row->getCellIterator();
176        while ($cells->valid()) {
177            /** @var \PhpOffice\PhpSpreadsheet\Cell\Cell $cell */
178            $cell = $cells->current();
179            $column = Coordinate::columnIndexFromString($cell->getColumn()) - 1;
180
181            $this->writeCellSpan($objWriter, $column, $prevColumn);
182            $objWriter->startElement('table:table-cell');
183            $this->writeCellMerge($objWriter, $cell);
184
185            // Style XF
186            $style = $cell->getXfIndex();
187            if ($style !== null) {
188                $objWriter->writeAttribute('table:style-name', self::CELL_STYLE_PREFIX . $style);
189            }
190
191            switch ($cell->getDataType()) {
192                case DataType::TYPE_BOOL:
193                    $objWriter->writeAttribute('office:value-type', 'boolean');
194                    $objWriter->writeAttribute('office:value', $cell->getValue());
195                    $objWriter->writeElement('text:p', $cell->getValue());
196
197                    break;
198                case DataType::TYPE_ERROR:
199                    throw new Exception('Writing of error not implemented yet.');
200
201                    break;
202                case DataType::TYPE_FORMULA:
203                    $formulaValue = $cell->getValue();
204                    if ($this->getParentWriter()->getPreCalculateFormulas()) {
205                        try {
206                            $formulaValue = $cell->getCalculatedValue();
207                        } catch (Exception $e) {
208                            // don't do anything
209                        }
210                    }
211                    $objWriter->writeAttribute('table:formula', $this->formulaConvertor->convertFormula($cell->getValue()));
212                    if (is_numeric($formulaValue)) {
213                        $objWriter->writeAttribute('office:value-type', 'float');
214                    } else {
215                        $objWriter->writeAttribute('office:value-type', 'string');
216                    }
217                    $objWriter->writeAttribute('office:value', $formulaValue);
218                    $objWriter->writeElement('text:p', $formulaValue);
219
220                    break;
221                case DataType::TYPE_INLINE:
222                    throw new Exception('Writing of inline not implemented yet.');
223
224                    break;
225                case DataType::TYPE_NUMERIC:
226                    $objWriter->writeAttribute('office:value-type', 'float');
227                    $objWriter->writeAttribute('office:value', $cell->getValue());
228                    $objWriter->writeElement('text:p', $cell->getValue());
229
230                    break;
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        foreach ($spreadsheet->getCellXfCollection() as $style) {
278            $writer->startElement('style:style');
279            $writer->writeAttribute('style:name', self::CELL_STYLE_PREFIX . $style->getIndex());
280            $writer->writeAttribute('style:family', 'table-cell');
281            $writer->writeAttribute('style:parent-style-name', 'Default');
282
283            // style:text-properties
284
285            // Font
286            $writer->startElement('style:text-properties');
287
288            $font = $style->getFont();
289
290            if ($font->getBold()) {
291                $writer->writeAttribute('fo:font-weight', 'bold');
292                $writer->writeAttribute('style:font-weight-complex', 'bold');
293                $writer->writeAttribute('style:font-weight-asian', 'bold');
294            }
295
296            if ($font->getItalic()) {
297                $writer->writeAttribute('fo:font-style', 'italic');
298            }
299
300            if ($color = $font->getColor()) {
301                $writer->writeAttribute('fo:color', sprintf('#%s', $color->getRGB()));
302            }
303
304            if ($family = $font->getName()) {
305                $writer->writeAttribute('fo:font-family', $family);
306            }
307
308            if ($size = $font->getSize()) {
309                $writer->writeAttribute('fo:font-size', sprintf('%.1Fpt', $size));
310            }
311
312            if ($font->getUnderline() && $font->getUnderline() != Font::UNDERLINE_NONE) {
313                $writer->writeAttribute('style:text-underline-style', 'solid');
314                $writer->writeAttribute('style:text-underline-width', 'auto');
315                $writer->writeAttribute('style:text-underline-color', 'font-color');
316
317                switch ($font->getUnderline()) {
318                    case Font::UNDERLINE_DOUBLE:
319                        $writer->writeAttribute('style:text-underline-type', 'double');
320
321                        break;
322                    case Font::UNDERLINE_SINGLE:
323                        $writer->writeAttribute('style:text-underline-type', 'single');
324
325                        break;
326                }
327            }
328
329            $writer->endElement(); // Close style:text-properties
330
331            // style:table-cell-properties
332
333            $writer->startElement('style:table-cell-properties');
334            $writer->writeAttribute('style:rotation-align', 'none');
335
336            // Fill
337            if ($fill = $style->getFill()) {
338                switch ($fill->getFillType()) {
339                    case Fill::FILL_SOLID:
340                        $writer->writeAttribute('fo:background-color', sprintf(
341                            '#%s',
342                            strtolower($fill->getStartColor()->getRGB())
343                        ));
344
345                        break;
346                    case Fill::FILL_GRADIENT_LINEAR:
347                    case Fill::FILL_GRADIENT_PATH:
348                        /// TODO :: To be implemented
349                        break;
350                    case Fill::FILL_NONE:
351                    default:
352                }
353            }
354
355            $writer->endElement(); // Close style:table-cell-properties
356
357            // End
358
359            $writer->endElement(); // Close style:style
360        }
361    }
362
363    /**
364     * Write attributes for merged cell.
365     */
366    private function writeCellMerge(XMLWriter $objWriter, Cell $cell): void
367    {
368        if (!$cell->isMergeRangeValueCell()) {
369            return;
370        }
371
372        $mergeRange = Coordinate::splitRange($cell->getMergeRange());
373        [$startCell, $endCell] = $mergeRange[0];
374        $start = Coordinate::coordinateFromString($startCell);
375        $end = Coordinate::coordinateFromString($endCell);
376        $columnSpan = Coordinate::columnIndexFromString($end[0]) - Coordinate::columnIndexFromString($start[0]) + 1;
377        $rowSpan = $end[1] - $start[1] + 1;
378
379        $objWriter->writeAttribute('table:number-columns-spanned', $columnSpan);
380        $objWriter->writeAttribute('table:number-rows-spanned', $rowSpan);
381    }
382}
383