Completed
Push — develop ( 7d4dc7...004a19 )
by Adrien
26:30
created

Html::getSheetIndex()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 4
rs 10
c 0
b 0
f 0
ccs 2
cts 2
cp 1
crap 1
1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Writer;
4
5
use PhpOffice\PhpSpreadsheet\Calculation;
6
use PhpOffice\PhpSpreadsheet\Cell;
7
use PhpOffice\PhpSpreadsheet\Chart;
8
use PhpOffice\PhpSpreadsheet\RichText;
9
use PhpOffice\PhpSpreadsheet\Shared\Drawing as SharedDrawing;
10
use PhpOffice\PhpSpreadsheet\Shared\File;
11
use PhpOffice\PhpSpreadsheet\Shared\Font as SharedFont;
12
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
13
use PhpOffice\PhpSpreadsheet\Spreadsheet;
14
use PhpOffice\PhpSpreadsheet\Style;
15
use PhpOffice\PhpSpreadsheet\Style\Alignment;
16
use PhpOffice\PhpSpreadsheet\Style\Border;
17
use PhpOffice\PhpSpreadsheet\Style\Borders;
18
use PhpOffice\PhpSpreadsheet\Style\Fill;
19
use PhpOffice\PhpSpreadsheet\Style\Font;
20
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
21
use PhpOffice\PhpSpreadsheet\Worksheet;
22
use PhpOffice\PhpSpreadsheet\Worksheet\Drawing;
23
use PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing;
24
use PhpOffice\PhpSpreadsheet\Writer\Exception as WriterException;
25
26
/**
27
 * Copyright (c) 2006 - 2015 Spreadsheet.
28
 *
29
 * This library is free software; you can redistribute it and/or
30
 * modify it under the terms of the GNU Lesser General Public
31
 * License as published by the Free Software Foundation; either
32
 * version 2.1 of the License, or (at your option) any later version.
33
 *
34
 * This library is distributed in the hope that it will be useful,
35
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
36
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
37
 * Lesser General Public License for more details.
38
 *
39
 * You should have received a copy of the GNU Lesser General Public
40
 * License along with this library; if not, write to the Free Software
41
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
42
 *
43
 * @category   Spreadsheet
44
 *
45
 * @copyright  Copyright (c) 2006 - 2015 Spreadsheet (https://github.com/PHPOffice/Spreadsheet)
46
 * @license    http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt    LGPL
47
 */
48
class Html extends BaseWriter implements IWriter
49
{
50
    /**
51
     * Spreadsheet object.
52
     *
53
     * @var Spreadsheet
54
     */
55
    protected $spreadsheet;
56
57
    /**
58
     * Sheet index to write.
59
     *
60
     * @var int
61
     */
62
    private $sheetIndex = 0;
63
64
    /**
65
     * Images root.
66
     *
67
     * @var string
68
     */
69
    private $imagesRoot = '';
70
71
    /**
72
     * embed images, or link to images.
73
     *
74
     * @var bool
75
     */
76
    private $embedImages = false;
77
78
    /**
79
     * Use inline CSS?
80
     *
81
     * @var bool
82
     */
83
    private $useInlineCss = false;
84
85
    /**
86
     * Array of CSS styles.
87
     *
88
     * @var array
89
     */
90
    private $cssStyles;
91
92
    /**
93
     * Array of column widths in points.
94
     *
95
     * @var array
96
     */
97
    private $columnWidths;
98
99
    /**
100
     * Default font.
101
     *
102
     * @var Font
103
     */
104
    private $defaultFont;
105
106
    /**
107
     * Flag whether spans have been calculated.
108
     *
109
     * @var bool
110
     */
111
    private $spansAreCalculated = false;
112
113
    /**
114
     * Excel cells that should not be written as HTML cells.
115
     *
116
     * @var array
117
     */
118
    private $isSpannedCell = [];
119
120
    /**
121
     * Excel cells that are upper-left corner in a cell merge.
122
     *
123
     * @var array
124
     */
125
    private $isBaseCell = [];
126
127
    /**
128
     * Excel rows that should not be written as HTML rows.
129
     *
130
     * @var array
131
     */
132
    private $isSpannedRow = [];
133
134
    /**
135
     * Is the current writer creating PDF?
136
     *
137
     * @var bool
138
     */
139
    protected $isPdf = false;
140
141
    /**
142
     * Generate the Navigation block.
143
     *
144
     * @var bool
145
     */
146
    private $generateSheetNavigationBlock = true;
147
148
    /**
149
     * Create a new HTML.
150
     *
151
     * @param Spreadsheet $spreadsheet
152
     */
153 6
    public function __construct(Spreadsheet $spreadsheet)
154
    {
155 6
        $this->spreadsheet = $spreadsheet;
156 6
        $this->defaultFont = $this->spreadsheet->getDefaultStyle()->getFont();
157 6
    }
158
159
    /**
160
     * Save Spreadsheet to file.
161
     *
162
     * @param string $pFilename
163
     *
164
     * @throws WriterException
165
     */
166 3
    public function save($pFilename)
167
    {
168
        // garbage collect
169 3
        $this->spreadsheet->garbageCollect();
170
171 3
        $saveDebugLog = Calculation::getInstance($this->spreadsheet)->getDebugLog()->getWriteDebugLog();
172 3
        Calculation::getInstance($this->spreadsheet)->getDebugLog()->setWriteDebugLog(false);
173 3
        $saveArrayReturnType = Calculation::getArrayReturnType();
174 3
        Calculation::setArrayReturnType(Calculation::RETURN_ARRAY_AS_VALUE);
175
176
        // Build CSS
177 3
        $this->buildCSS(!$this->useInlineCss);
178
179
        // Open file
180 3
        $fileHandle = fopen($pFilename, 'wb+');
181 3
        if ($fileHandle === false) {
182
            throw new WriterException("Could not open file $pFilename for writing.");
183
        }
184
185
        // Write headers
186 3
        fwrite($fileHandle, $this->generateHTMLHeader(!$this->useInlineCss));
187
188
        // Write navigation (tabs)
189 3
        if ((!$this->isPdf) && ($this->generateSheetNavigationBlock)) {
190 3
            fwrite($fileHandle, $this->generateNavigation());
191
        }
192
193
        // Write data
194 3
        fwrite($fileHandle, $this->generateSheetData());
195
196
        // Write footer
197 3
        fwrite($fileHandle, $this->generateHTMLFooter());
198
199
        // Close file
200 3
        fclose($fileHandle);
201
202 3
        Calculation::setArrayReturnType($saveArrayReturnType);
203 3
        Calculation::getInstance($this->spreadsheet)->getDebugLog()->setWriteDebugLog($saveDebugLog);
204 3
    }
205
206
    /**
207
     * Map VAlign.
208
     *
209
     * @param string $vAlign Vertical alignment
210
     *
211
     * @return string
212
     */
213 6
    private function mapVAlign($vAlign)
214
    {
215
        switch ($vAlign) {
216 6
            case Alignment::VERTICAL_BOTTOM:
217 6
                return 'bottom';
218 4
            case Alignment::VERTICAL_TOP:
219
                return 'top';
220 4
            case Alignment::VERTICAL_CENTER:
221
            case Alignment::VERTICAL_JUSTIFY:
222 4
                return 'middle';
223
            default:
224
                return 'baseline';
225
        }
226
    }
227
228
    /**
229
     * Map HAlign.
230
     *
231
     * @param string $hAlign Horizontal alignment
232
     *
233
     * @return string|false
234
     */
235 6
    private function mapHAlign($hAlign)
236
    {
237
        switch ($hAlign) {
238 6
            case Alignment::HORIZONTAL_GENERAL:
239 6
                return false;
240 5
            case Alignment::HORIZONTAL_LEFT:
241 4
                return 'left';
242 5
            case Alignment::HORIZONTAL_RIGHT:
243 4
                return 'right';
244 5
            case Alignment::HORIZONTAL_CENTER:
245 4
            case Alignment::HORIZONTAL_CENTER_CONTINUOUS:
246 1
                return 'center';
247 4
            case Alignment::HORIZONTAL_JUSTIFY:
248 4
                return 'justify';
249
            default:
250
                return false;
251
        }
252
    }
253
254
    /**
255
     * Map border style.
256
     *
257
     * @param int $borderStyle Sheet index
258
     *
259
     * @return string
260
     */
261 6
    private function mapBorderStyle($borderStyle)
262
    {
263
        switch ($borderStyle) {
264 6
            case Border::BORDER_NONE:
265 6
                return 'none';
266 4
            case Border::BORDER_DASHDOT:
267
                return '1px dashed';
268 4
            case Border::BORDER_DASHDOTDOT:
269
                return '1px dotted';
270 4
            case Border::BORDER_DASHED:
271
                return '1px dashed';
272 4
            case Border::BORDER_DOTTED:
273
                return '1px dotted';
274 4
            case Border::BORDER_DOUBLE:
275
                return '3px double';
276 4
            case Border::BORDER_HAIR:
277
                return '1px solid';
278 4
            case Border::BORDER_MEDIUM:
279
                return '2px solid';
280 4
            case Border::BORDER_MEDIUMDASHDOT:
281
                return '2px dashed';
282 4
            case Border::BORDER_MEDIUMDASHDOTDOT:
283
                return '2px dotted';
284 4
            case Border::BORDER_MEDIUMDASHED:
285
                return '2px dashed';
286 4
            case Border::BORDER_SLANTDASHDOT:
287
                return '2px dashed';
288 4
            case Border::BORDER_THICK:
289 4
                return '3px solid';
290 4
            case Border::BORDER_THIN:
291 4
                return '1px solid';
292
            default:
293
                // map others to thin
294
                return '1px solid';
295
        }
296
    }
297
298
    /**
299
     * Get sheet index.
300
     *
301
     * @return int
302
     */
303 4
    public function getSheetIndex()
304
    {
305 4
        return $this->sheetIndex;
306
    }
307
308
    /**
309
     * Set sheet index.
310
     *
311
     * @param int $pValue Sheet index
312
     *
313
     * @return HTML
314
     */
315
    public function setSheetIndex($pValue)
316
    {
317
        $this->sheetIndex = $pValue;
318
319
        return $this;
320
    }
321
322
    /**
323
     * Get sheet index.
324
     *
325
     * @return bool
326
     */
327
    public function getGenerateSheetNavigationBlock()
328
    {
329
        return $this->generateSheetNavigationBlock;
330
    }
331
332
    /**
333
     * Set sheet index.
334
     *
335
     * @param bool $pValue Flag indicating whether the sheet navigation block should be generated or not
336
     *
337
     * @return HTML
338
     */
339
    public function setGenerateSheetNavigationBlock($pValue)
340
    {
341
        $this->generateSheetNavigationBlock = (bool) $pValue;
342
343
        return $this;
344
    }
345
346
    /**
347
     * Write all sheets (resets sheetIndex to NULL).
348
     */
349
    public function writeAllSheets()
350
    {
351
        $this->sheetIndex = null;
352
353
        return $this;
354
    }
355
356
    /**
357
     * Generate HTML header.
358
     *
359
     * @param bool $pIncludeStyles Include styles?
360
     *
361
     * @throws WriterException
362
     *
363
     * @return string
364
     */
365 6
    public function generateHTMLHeader($pIncludeStyles = false)
366
    {
367
        // Spreadsheet object known?
368 6
        if (is_null($this->spreadsheet)) {
369
            throw new WriterException('Internal Spreadsheet object not set to an instance of an object.');
370
        }
371
372
        // Construct HTML
373 6
        $properties = $this->spreadsheet->getProperties();
374 6
        $html = '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">' . PHP_EOL;
375 6
        $html .= '<!-- Generated by Spreadsheet - https://github.com/PHPOffice/Spreadsheet -->' . PHP_EOL;
376 6
        $html .= '<html>' . PHP_EOL;
377 6
        $html .= '  <head>' . PHP_EOL;
378 6
        $html .= '      <meta http-equiv="Content-Type" content="text/html; charset=utf-8">' . PHP_EOL;
379 6
        if ($properties->getTitle() > '') {
380 6
            $html .= '      <title>' . htmlspecialchars($properties->getTitle()) . '</title>' . PHP_EOL;
381
        }
382 6
        if ($properties->getCreator() > '') {
383 6
            $html .= '      <meta name="author" content="' . htmlspecialchars($properties->getCreator()) . '" />' . PHP_EOL;
384
        }
385 6
        if ($properties->getTitle() > '') {
386 6
            $html .= '      <meta name="title" content="' . htmlspecialchars($properties->getTitle()) . '" />' . PHP_EOL;
387
        }
388 6 View Code Duplication
        if ($properties->getDescription() > '') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
389 5
            $html .= '      <meta name="description" content="' . htmlspecialchars($properties->getDescription()) . '" />' . PHP_EOL;
390
        }
391 6
        if ($properties->getSubject() > '') {
392 6
            $html .= '      <meta name="subject" content="' . htmlspecialchars($properties->getSubject()) . '" />' . PHP_EOL;
393
        }
394 6
        if ($properties->getKeywords() > '') {
395 6
            $html .= '      <meta name="keywords" content="' . htmlspecialchars($properties->getKeywords()) . '" />' . PHP_EOL;
396
        }
397 6
        if ($properties->getCategory() > '') {
398 6
            $html .= '      <meta name="category" content="' . htmlspecialchars($properties->getCategory()) . '" />' . PHP_EOL;
399
        }
400 6 View Code Duplication
        if ($properties->getCompany() > '') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
401 6
            $html .= '      <meta name="company" content="' . htmlspecialchars($properties->getCompany()) . '" />' . PHP_EOL;
402
        }
403 6 View Code Duplication
        if ($properties->getManager() > '') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
404
            $html .= '      <meta name="manager" content="' . htmlspecialchars($properties->getManager()) . '" />' . PHP_EOL;
405
        }
406
407 6
        if ($pIncludeStyles) {
408 3
            $html .= $this->generateStyles(true);
409
        }
410
411 6
        $html .= '  </head>' . PHP_EOL;
412 6
        $html .= '' . PHP_EOL;
413 6
        $html .= '  <body>' . PHP_EOL;
414
415 6
        return $html;
416
    }
417
418
    /**
419
     * Generate sheet data.
420
     *
421
     * @throws WriterException
422
     *
423
     * @return string
424
     */
425 6
    public function generateSheetData()
426
    {
427
        // Spreadsheet object known?
428 6
        if (is_null($this->spreadsheet)) {
429
            throw new WriterException('Internal Spreadsheet object not set to an instance of an object.');
430
        }
431
432
        // Ensure that Spans have been calculated?
433 6
        if ($this->sheetIndex !== null || !$this->spansAreCalculated) {
434 6
            $this->calculateSpans();
435
        }
436
437
        // Fetch sheets
438 6
        $sheets = [];
439 6 View Code Duplication
        if (is_null($this->sheetIndex)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
440
            $sheets = $this->spreadsheet->getAllSheets();
441
        } else {
442 6
            $sheets[] = $this->spreadsheet->getSheet($this->sheetIndex);
443
        }
444
445
        // Construct HTML
446 6
        $html = '';
447
448
        // Loop all sheets
449 6
        $sheetId = 0;
450 6
        foreach ($sheets as $sheet) {
451
            // Write table header
452 6
            $html .= $this->generateTableHeader($sheet);
453
454
            // Get worksheet dimension
455 6
            $dimension = explode(':', $sheet->calculateWorksheetDimension());
456 6
            $dimension[0] = Cell::coordinateFromString($dimension[0]);
457 6
            $dimension[0][0] = Cell::columnIndexFromString($dimension[0][0]) - 1;
458 6
            $dimension[1] = Cell::coordinateFromString($dimension[1]);
459 6
            $dimension[1][0] = Cell::columnIndexFromString($dimension[1][0]) - 1;
460
461
            // row min,max
462 6
            $rowMin = $dimension[0][1];
463 6
            $rowMax = $dimension[1][1];
464
465
            // calculate start of <tbody>, <thead>
466 6
            $tbodyStart = $rowMin;
467 6
            $theadStart = $theadEnd = 0; // default: no <thead>    no </thead>
468 6
            if ($sheet->getPageSetup()->isRowsToRepeatAtTopSet()) {
469
                $rowsToRepeatAtTop = $sheet->getPageSetup()->getRowsToRepeatAtTop();
470
471
                // we can only support repeating rows that start at top row
472
                if ($rowsToRepeatAtTop[0] == 1) {
473
                    $theadStart = $rowsToRepeatAtTop[0];
474
                    $theadEnd = $rowsToRepeatAtTop[1];
475
                    $tbodyStart = $rowsToRepeatAtTop[1] + 1;
476
                }
477
            }
478
479
            // Loop through cells
480 6
            $row = $rowMin - 1;
481 6
            while ($row++ < $rowMax) {
482
                // <thead> ?
483 6
                if ($row == $theadStart) {
484
                    $html .= '        <thead>' . PHP_EOL;
485
                    $cellType = 'th';
486
                }
487
488
                // <tbody> ?
489 6
                if ($row == $tbodyStart) {
490 6
                    $html .= '        <tbody>' . PHP_EOL;
491 6
                    $cellType = 'td';
492
                }
493
494
                // Write row if there are HTML table cells in it
495 6
                if (!isset($this->isSpannedRow[$sheet->getParent()->getIndex($sheet)][$row])) {
496
                    // Start a new rowData
497 6
                    $rowData = [];
498
                    // Loop through columns
499 6
                    $column = $dimension[0][0] - 1;
500 6
                    while ($column++ < $dimension[1][0]) {
501
                        // Cell exists?
502 6
                        if ($sheet->cellExistsByColumnAndRow($column, $row)) {
503 6
                            $rowData[$column] = Cell::stringFromColumnIndex($column) . $row;
504
                        } else {
505 4
                            $rowData[$column] = '';
506
                        }
507
                    }
508 6
                    $html .= $this->generateRow($sheet, $rowData, $row - 1, $cellType);
0 ignored issues
show
Bug introduced by
The variable $cellType does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
509
                }
510
511
                // </thead> ?
512 6
                if ($row == $theadEnd) {
513
                    $html .= '        </thead>' . PHP_EOL;
514
                }
515
            }
516 6
            $html .= $this->extendRowsForChartsAndImages($sheet, $row);
517
518
            // Close table body.
519 6
            $html .= '        </tbody>' . PHP_EOL;
520
521
            // Write table footer
522 6
            $html .= $this->generateTableFooter();
523
524
            // Writing PDF?
525 6
            if ($this->isPdf) {
526 4
                if (is_null($this->sheetIndex) && $sheetId + 1 < $this->spreadsheet->getSheetCount()) {
527
                    $html .= '<div style="page-break-before:always" />';
528
                }
529
            }
530
531
            // Next sheet
532 6
            ++$sheetId;
533
        }
534
535 6
        return $html;
536
    }
537
538
    /**
539
     * Generate sheet tabs.
540
     *
541
     * @throws WriterException
542
     *
543
     * @return string
544
     */
545 3
    public function generateNavigation()
546
    {
547
        // Spreadsheet object known?
548 3
        if (is_null($this->spreadsheet)) {
549
            throw new WriterException('Internal Spreadsheet object not set to an instance of an object.');
550
        }
551
552
        // Fetch sheets
553 3
        $sheets = [];
554 3 View Code Duplication
        if (is_null($this->sheetIndex)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
555
            $sheets = $this->spreadsheet->getAllSheets();
556
        } else {
557 3
            $sheets[] = $this->spreadsheet->getSheet($this->sheetIndex);
558
        }
559
560
        // Construct HTML
561 3
        $html = '';
562
563
        // Only if there are more than 1 sheets
564 3
        if (count($sheets) > 1) {
565
            // Loop all sheets
566
            $sheetId = 0;
567
568
            $html .= '<ul class="navigation">' . PHP_EOL;
569
570
            foreach ($sheets as $sheet) {
571
                $html .= '  <li class="sheet' . $sheetId . '"><a href="#sheet' . $sheetId . '">' . $sheet->getTitle() . '</a></li>' . PHP_EOL;
572
                ++$sheetId;
573
            }
574
575
            $html .= '</ul>' . PHP_EOL;
576
        }
577
578 3
        return $html;
579
    }
580
581 6
    private function extendRowsForChartsAndImages(Worksheet $pSheet, $row)
582
    {
583 6
        $rowMax = $row;
584 6
        $colMax = 'A';
585 6
        if ($this->includeCharts) {
586
            foreach ($pSheet->getChartCollection() as $chart) {
587
                if ($chart instanceof Chart) {
588
                    $chartCoordinates = $chart->getTopLeftPosition();
589
                    $chartTL = Cell::coordinateFromString($chartCoordinates['cell']);
590
                    $chartCol = Cell::columnIndexFromString($chartTL[0]);
591
                    if ($chartTL[1] > $rowMax) {
592
                        $rowMax = $chartTL[1];
593
                        if ($chartCol > Cell::columnIndexFromString($colMax)) {
594
                            $colMax = $chartTL[0];
595
                        }
596
                    }
597
                }
598
            }
599
        }
600
601 6
        foreach ($pSheet->getDrawingCollection() as $drawing) {
602 5
            if ($drawing instanceof Drawing) {
603 4
                $imageTL = Cell::coordinateFromString($drawing->getCoordinates());
604 4
                $imageCol = Cell::columnIndexFromString($imageTL[0]);
605 4
                if ($imageTL[1] > $rowMax) {
606
                    $rowMax = $imageTL[1];
607
                    if ($imageCol > Cell::columnIndexFromString($colMax)) {
608 5
                        $colMax = $imageTL[0];
609
                    }
610
                }
611
            }
612
        }
613
614
        // Don't extend rows if not needed
615 6
        if ($row === $rowMax) {
616 6
            return '';
617
        }
618
619
        $html = '';
620
        ++$colMax;
621
622
        while ($row <= $rowMax) {
623
            $html .= '<tr>';
624
            for ($col = 'A'; $col != $colMax; ++$col) {
625
                $html .= '<td>';
626
                $html .= $this->writeImageInCell($pSheet, $col . $row);
627
                if ($this->includeCharts) {
628
                    $html .= $this->writeChartInCell($pSheet, $col . $row);
629
                }
630
                $html .= '</td>';
631
            }
632
            ++$row;
633
            $html .= '</tr>';
634
        }
635
636
        return $html;
637
    }
638
639
    /**
640
     * Generate image tag in cell.
641
     *
642
     * @param Worksheet $pSheet \PhpOffice\PhpSpreadsheet\Worksheet
643
     * @param string $coordinates Cell coordinates
644
     *
645
     * @throws WriterException
646
     *
647
     * @return string
648
     */
649 6
    private function writeImageInCell(Worksheet $pSheet, $coordinates)
650
    {
651
        // Construct HTML
652 6
        $html = '';
653
654
        // Write images
655 6
        foreach ($pSheet->getDrawingCollection() as $drawing) {
656 5
            if ($drawing instanceof Drawing) {
657 4
                if ($drawing->getCoordinates() == $coordinates) {
658 4
                    $filename = $drawing->getPath();
659
660
                    // Strip off eventual '.'
661 4
                    if (substr($filename, 0, 1) == '.') {
662
                        $filename = substr($filename, 1);
663
                    }
664
665
                    // Prepend images root
666 4
                    $filename = $this->getImagesRoot() . $filename;
667
668
                    // Strip off eventual '.'
669 4
                    if (substr($filename, 0, 1) == '.' && substr($filename, 0, 2) != './') {
670
                        $filename = substr($filename, 1);
671
                    }
672
673
                    // Convert UTF8 data to PCDATA
674 4
                    $filename = htmlspecialchars($filename);
675
676 4
                    $html .= PHP_EOL;
677 4
                    if ((!$this->embedImages) || ($this->isPdf)) {
678 4
                        $imageData = $filename;
679
                    } else {
680
                        $imageDetails = getimagesize($filename);
681
                        if ($fp = fopen($filename, 'rb', 0)) {
682
                            $picture = fread($fp, filesize($filename));
683
                            fclose($fp);
684
                            // base64 encode the binary data, then break it
685
                            // into chunks according to RFC 2045 semantics
686
                            $base64 = chunk_split(base64_encode($picture));
687
                            $imageData = 'data:' . $imageDetails['mime'] . ';base64,' . $base64;
688
                        } else {
689
                            $imageData = $filename;
690
                        }
691
                    }
692
693 4
                    $html .= '<div style="position: relative;">';
694
                    $html .= '<img style="position: absolute; z-index: 1; left: ' .
695 4
                        $drawing->getOffsetX() . 'px; top: ' . $drawing->getOffsetY() . 'px; width: ' .
696 4
                        $drawing->getWidth() . 'px; height: ' . $drawing->getHeight() . 'px;" src="' .
697 4
                        $imageData . '" border="0" />';
698 4
                    $html .= '</div>';
699
                }
700
            } elseif ($drawing instanceof MemoryDrawing) {
701 1
                if ($drawing->getCoordinates() != $coordinates) {
702
                    continue;
703
                }
704 1
                ob_start(); //  Let's start output buffering.
705 1
                imagepng($drawing->getImageResource()); //  This will normally output the image, but because of ob_start(), it won't.
706 1
                $contents = ob_get_contents(); //  Instead, output above is saved to $contents
707 1
                ob_end_clean(); //  End the output buffer.
708
709 1
                $dataUri = 'data:image/jpeg;base64,' . base64_encode($contents);
710
711
                //  Because of the nature of tables, width is more important than height.
712
                //  max-width: 100% ensures that image doesnt overflow containing cell
713
                //  width: X sets width of supplied image.
714
                //  As a result, images bigger than cell will be contained and images smaller will not get stretched
715 5
                $html .= '<img src="' . $dataUri . '" style="max-width:100%;width:' . $drawing->getWidth() . 'px;" />';
716
            }
717
        }
718
719 6
        return $html;
720
    }
721
722
    /**
723
     * Generate chart tag in cell.
724
     *
725
     * @param Worksheet $pSheet \PhpOffice\PhpSpreadsheet\Worksheet
726
     * @param string $coordinates Cell coordinates
727
     *
728
     * @throws WriterException
729
     *
730
     * @return string
731
     */
732
    private function writeChartInCell(Worksheet $pSheet, $coordinates)
733
    {
734
        // Construct HTML
735
        $html = '';
736
737
        // Write charts
738
        foreach ($pSheet->getChartCollection() as $chart) {
739
            if ($chart instanceof Chart) {
740
                $chartCoordinates = $chart->getTopLeftPosition();
741
                if ($chartCoordinates['cell'] == $coordinates) {
742
                    $chartFileName = File::sysGetTempDir() . '/' . uniqid('', true) . '.png';
743
                    if (!$chart->render($chartFileName)) {
744
                        return;
745
                    }
746
747
                    $html .= PHP_EOL;
748
                    $imageDetails = getimagesize($chartFileName);
749
                    if ($fp = fopen($chartFileName, 'rb', 0)) {
750
                        $picture = fread($fp, filesize($chartFileName));
751
                        fclose($fp);
752
                        // base64 encode the binary data, then break it
753
                        // into chunks according to RFC 2045 semantics
754
                        $base64 = chunk_split(base64_encode($picture));
755
                        $imageData = 'data:' . $imageDetails['mime'] . ';base64,' . $base64;
756
757
                        $html .= '<div style="position: relative;">';
758
                        $html .= '<img style="position: absolute; z-index: 1; left: ' . $chartCoordinates['xOffset'] . 'px; top: ' . $chartCoordinates['yOffset'] . 'px; width: ' . $imageDetails[0] . 'px; height: ' . $imageDetails[1] . 'px;" src="' . $imageData . '" border="0" />' . PHP_EOL;
759
                        $html .= '</div>';
760
761
                        unlink($chartFileName);
762
                    }
763
                }
764
            }
765
        }
766
767
        // Return
768
        return $html;
769
    }
770
771
    /**
772
     * Generate CSS styles.
773
     *
774
     * @param bool $generateSurroundingHTML Generate surrounding HTML tags? (&lt;style&gt; and &lt;/style&gt;)
775
     *
776
     * @throws WriterException
777
     *
778
     * @return string
779
     */
780 3
    public function generateStyles($generateSurroundingHTML = true)
781
    {
782
        // Spreadsheet object known?
783 3
        if (is_null($this->spreadsheet)) {
784
            throw new WriterException('Internal Spreadsheet object not set to an instance of an object.');
785
        }
786
787
        // Build CSS
788 3
        $css = $this->buildCSS($generateSurroundingHTML);
789
790
        // Construct HTML
791 3
        $html = '';
792
793
        // Start styles
794 3
        if ($generateSurroundingHTML) {
795 3
            $html .= '    <style type="text/css">' . PHP_EOL;
796 3
            $html .= '      html { ' . $this->assembleCSS($css['html']) . ' }' . PHP_EOL;
797
        }
798
799
        // Write all other styles
800 3
        foreach ($css as $styleName => $styleDefinition) {
801 3
            if ($styleName != 'html') {
802 3
                $html .= '      ' . $styleName . ' { ' . $this->assembleCSS($styleDefinition) . ' }' . PHP_EOL;
803
            }
804
        }
805
806
        // End styles
807 3
        if ($generateSurroundingHTML) {
808 3
            $html .= '    </style>' . PHP_EOL;
809
        }
810
811
        // Return
812 3
        return $html;
813
    }
814
815
    /**
816
     * Build CSS styles.
817
     *
818
     * @param bool $generateSurroundingHTML Generate surrounding HTML style? (html { })
819
     *
820
     * @throws WriterException
821
     *
822
     * @return array
823
     */
824 6
    public function buildCSS($generateSurroundingHTML = true)
825
    {
826
        // Spreadsheet object known?
827 6
        if (is_null($this->spreadsheet)) {
828
            throw new WriterException('Internal Spreadsheet object not set to an instance of an object.');
829
        }
830
831
        // Cached?
832 6
        if (!is_null($this->cssStyles)) {
833 3
            return $this->cssStyles;
834
        }
835
836
        // Ensure that spans have been calculated
837 6
        if (!$this->spansAreCalculated) {
838 6
            $this->calculateSpans();
839
        }
840
841
        // Construct CSS
842 6
        $css = [];
843
844
        // Start styles
845 6
        if ($generateSurroundingHTML) {
846
            // html { }
847 6
            $css['html']['font-family'] = 'Calibri, Arial, Helvetica, sans-serif';
848 6
            $css['html']['font-size'] = '11pt';
849 6
            $css['html']['background-color'] = 'white';
850
        }
851
852
        // table { }
853 6
        $css['table']['border-collapse'] = 'collapse';
854 6
        if (!$this->isPdf) {
855 3
            $css['table']['page-break-after'] = 'always';
856
        }
857
858
        // .gridlines td { }
859 6
        $css['.gridlines td']['border'] = '1px dotted black';
860 6
        $css['.gridlines th']['border'] = '1px dotted black';
861
862
        // .b {}
863 6
        $css['.b']['text-align'] = 'center'; // BOOL
864
865
        // .e {}
866 6
        $css['.e']['text-align'] = 'center'; // ERROR
867
868
        // .f {}
869 6
        $css['.f']['text-align'] = 'right'; // FORMULA
870
871
        // .inlineStr {}
872 6
        $css['.inlineStr']['text-align'] = 'left'; // INLINE
873
874
        // .n {}
875 6
        $css['.n']['text-align'] = 'right'; // NUMERIC
876
877
        // .s {}
878 6
        $css['.s']['text-align'] = 'left'; // STRING
879
880
        // Calculate cell style hashes
881 6
        foreach ($this->spreadsheet->getCellXfCollection() as $index => $style) {
882 6
            $css['td.style' . $index] = $this->createCSSStyle($style);
883 6
            $css['th.style' . $index] = $this->createCSSStyle($style);
884
        }
885
886
        // Fetch sheets
887 6
        $sheets = [];
888 6 View Code Duplication
        if (is_null($this->sheetIndex)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
889
            $sheets = $this->spreadsheet->getAllSheets();
890
        } else {
891 6
            $sheets[] = $this->spreadsheet->getSheet($this->sheetIndex);
892
        }
893
894
        // Build styles per sheet
895 6
        foreach ($sheets as $sheet) {
896
            // Calculate hash code
897 6
            $sheetIndex = $sheet->getParent()->getIndex($sheet);
898
899
            // Build styles
900
            // Calculate column widths
901 6
            $sheet->calculateColumnWidths();
902
903
            // col elements, initialize
904 6
            $highestColumnIndex = Cell::columnIndexFromString($sheet->getHighestColumn()) - 1;
905 6
            $column = -1;
906 6
            while ($column++ < $highestColumnIndex) {
907 6
                $this->columnWidths[$sheetIndex][$column] = 42; // approximation
908 6
                $css['table.sheet' . $sheetIndex . ' col.col' . $column]['width'] = '42pt';
909
            }
910
911
            // col elements, loop through columnDimensions and set width
912 6
            foreach ($sheet->getColumnDimensions() as $columnDimension) {
913 5
                if (($width = SharedDrawing::cellDimensionToPixels($columnDimension->getWidth(), $this->defaultFont)) >= 0) {
914 5
                    $width = SharedDrawing::pixelsToPoints($width);
915 5
                    $column = Cell::columnIndexFromString($columnDimension->getColumnIndex()) - 1;
916 5
                    $this->columnWidths[$sheetIndex][$column] = $width;
917 5
                    $css['table.sheet' . $sheetIndex . ' col.col' . $column]['width'] = $width . 'pt';
918
919 5 View Code Duplication
                    if ($columnDimension->getVisible() === false) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
920
                        $css['table.sheet' . $sheetIndex . ' col.col' . $column]['visibility'] = 'collapse';
921 5
                        $css['table.sheet' . $sheetIndex . ' col.col' . $column]['*display'] = 'none'; // target IE6+7
922
                    }
923
                }
924
            }
925
926
            // Default row height
927 6
            $rowDimension = $sheet->getDefaultRowDimension();
928
929
            // table.sheetN tr { }
930 6
            $css['table.sheet' . $sheetIndex . ' tr'] = [];
931
932 6 View Code Duplication
            if ($rowDimension->getRowHeight() == -1) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
933 6
                $pt_height = SharedFont::getDefaultRowHeightByFont($this->spreadsheet->getDefaultStyle()->getFont());
934
            } else {
935
                $pt_height = $rowDimension->getRowHeight();
936
            }
937 6
            $css['table.sheet' . $sheetIndex . ' tr']['height'] = $pt_height . 'pt';
938 6
            if ($rowDimension->getVisible() === false) {
939
                $css['table.sheet' . $sheetIndex . ' tr']['display'] = 'none';
940
                $css['table.sheet' . $sheetIndex . ' tr']['visibility'] = 'hidden';
941
            }
942
943
            // Calculate row heights
944 6
            foreach ($sheet->getRowDimensions() as $rowDimension) {
945 2
                $row = $rowDimension->getRowIndex() - 1;
946
947
                // table.sheetN tr.rowYYYYYY { }
948 2
                $css['table.sheet' . $sheetIndex . ' tr.row' . $row] = [];
949
950 2 View Code Duplication
                if ($rowDimension->getRowHeight() == -1) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
951 2
                    $pt_height = SharedFont::getDefaultRowHeightByFont($this->spreadsheet->getDefaultStyle()->getFont());
952
                } else {
953 1
                    $pt_height = $rowDimension->getRowHeight();
954
                }
955 2
                $css['table.sheet' . $sheetIndex . ' tr.row' . $row]['height'] = $pt_height . 'pt';
956 2 View Code Duplication
                if ($rowDimension->getVisible() === false) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
957
                    $css['table.sheet' . $sheetIndex . ' tr.row' . $row]['display'] = 'none';
958 6
                    $css['table.sheet' . $sheetIndex . ' tr.row' . $row]['visibility'] = 'hidden';
959
                }
960
            }
961
        }
962
963
        // Cache
964 6
        if (is_null($this->cssStyles)) {
965 6
            $this->cssStyles = $css;
966
        }
967
968
        // Return
969 6
        return $css;
970
    }
971
972
    /**
973
     * Create CSS style.
974
     *
975
     * @param Style $pStyle
976
     *
977
     * @return array
978
     */
979 6
    private function createCSSStyle(Style $pStyle)
980
    {
981
        // Create CSS
982 6
        $css = array_merge(
983 6
            $this->createCSSStyleAlignment($pStyle->getAlignment()),
984 6
            $this->createCSSStyleBorders($pStyle->getBorders()),
985 6
            $this->createCSSStyleFont($pStyle->getFont()),
986 6
            $this->createCSSStyleFill($pStyle->getFill())
987
        );
988
989
        // Return
990 6
        return $css;
991
    }
992
993
    /**
994
     * Create CSS style (\PhpOffice\PhpSpreadsheet\Style\Alignment).
995
     *
996
     * @param Alignment $pStyle \PhpOffice\PhpSpreadsheet\Style\Alignment
997
     *
998
     * @return array
999
     */
1000 6
    private function createCSSStyleAlignment(Alignment $pStyle)
1001
    {
1002
        // Construct CSS
1003 6
        $css = [];
1004
1005
        // Create CSS
1006 6
        $css['vertical-align'] = $this->mapVAlign($pStyle->getVertical());
1007 6
        if ($textAlign = $this->mapHAlign($pStyle->getHorizontal())) {
1008 5
            $css['text-align'] = $textAlign;
1009 5
            if (in_array($textAlign, ['left', 'right'])) {
1010 4
                $css['padding-' . $textAlign] = (string) ((int) $pStyle->getIndent() * 9) . 'px';
1011
            }
1012
        }
1013
1014 6
        return $css;
1015
    }
1016
1017
    /**
1018
     * Create CSS style (\PhpOffice\PhpSpreadsheet\Style\Font).
1019
     *
1020
     * @param Font $pStyle
1021
     *
1022
     * @return array
1023
     */
1024 6
    private function createCSSStyleFont(Font $pStyle)
1025
    {
1026
        // Construct CSS
1027 6
        $css = [];
1028
1029
        // Create CSS
1030 6
        if ($pStyle->getBold()) {
1031 5
            $css['font-weight'] = 'bold';
1032
        }
1033 6
        if ($pStyle->getUnderline() != Font::UNDERLINE_NONE && $pStyle->getStrikethrough()) {
1034
            $css['text-decoration'] = 'underline line-through';
1035 6
        } elseif ($pStyle->getUnderline() != Font::UNDERLINE_NONE) {
1036 4
            $css['text-decoration'] = 'underline';
1037 6
        } elseif ($pStyle->getStrikethrough()) {
1038
            $css['text-decoration'] = 'line-through';
1039
        }
1040 6
        if ($pStyle->getItalic()) {
1041 4
            $css['font-style'] = 'italic';
1042
        }
1043
1044 6
        $css['color'] = '#' . $pStyle->getColor()->getRGB();
1045 6
        $css['font-family'] = '\'' . $pStyle->getName() . '\'';
1046 6
        $css['font-size'] = $pStyle->getSize() . 'pt';
1047
1048 6
        return $css;
1049
    }
1050
1051
    /**
1052
     * Create CSS style (Borders).
1053
     *
1054
     * @param Borders $pStyle Borders
1055
     *
1056
     * @return array
1057
     */
1058 6
    private function createCSSStyleBorders(Borders $pStyle)
1059
    {
1060
        // Construct CSS
1061 6
        $css = [];
1062
1063
        // Create CSS
1064 6
        $css['border-bottom'] = $this->createCSSStyleBorder($pStyle->getBottom());
1065 6
        $css['border-top'] = $this->createCSSStyleBorder($pStyle->getTop());
1066 6
        $css['border-left'] = $this->createCSSStyleBorder($pStyle->getLeft());
1067 6
        $css['border-right'] = $this->createCSSStyleBorder($pStyle->getRight());
1068
1069 6
        return $css;
1070
    }
1071
1072
    /**
1073
     * Create CSS style (Border).
1074
     *
1075
     * @param Border $pStyle Border
1076
     *
1077
     * @return string
1078
     */
1079 6
    private function createCSSStyleBorder(Border $pStyle)
1080
    {
1081
        //    Create CSS - add !important to non-none border styles for merged cells
1082 6
        $borderStyle = $this->mapBorderStyle($pStyle->getBorderStyle());
1083 6
        $css = $borderStyle . ' #' . $pStyle->getColor()->getRGB() . (($borderStyle == 'none') ? '' : ' !important');
1084
1085 6
        return $css;
1086
    }
1087
1088
    /**
1089
     * Create CSS style (Fill).
1090
     *
1091
     * @param Fill $pStyle Fill
1092
     *
1093
     * @return array
1094
     */
1095 6
    private function createCSSStyleFill(Fill $pStyle)
1096
    {
1097
        // Construct HTML
1098 6
        $css = [];
1099
1100
        // Create CSS
1101 6
        $value = $pStyle->getFillType() == Fill::FILL_NONE ?
1102 6
            'white' : '#' . $pStyle->getStartColor()->getRGB();
1103 6
        $css['background-color'] = $value;
1104
1105 6
        return $css;
1106
    }
1107
1108
    /**
1109
     * Generate HTML footer.
1110
     */
1111 6
    public function generateHTMLFooter()
1112
    {
1113
        // Construct HTML
1114 6
        $html = '';
1115 6
        $html .= '  </body>' . PHP_EOL;
1116 6
        $html .= '</html>' . PHP_EOL;
1117
1118 6
        return $html;
1119
    }
1120
1121
    /**
1122
     * Generate table header.
1123
     *
1124
     * @param Worksheet $pSheet The worksheet for the table we are writing
1125
     *
1126
     * @throws WriterException
1127
     *
1128
     * @return string
1129
     */
1130 6
    private function generateTableHeader($pSheet)
1131
    {
1132 6
        $sheetIndex = $pSheet->getParent()->getIndex($pSheet);
1133
1134
        // Construct HTML
1135 6
        $html = '';
1136 6
        $html .= $this->setMargins($pSheet);
1137
1138 6
        if (!$this->useInlineCss) {
1139 3
            $gridlines = $pSheet->getShowGridlines() ? ' gridlines' : '';
1140 3
            $html .= '    <table border="0" cellpadding="0" cellspacing="0" id="sheet' . $sheetIndex . '" class="sheet' . $sheetIndex . $gridlines . '">' . PHP_EOL;
1141
        } else {
1142 4
            $style = isset($this->cssStyles['table']) ?
1143 4
                $this->assembleCSS($this->cssStyles['table']) : '';
1144
1145 4
            if ($this->isPdf && $pSheet->getShowGridlines()) {
1146 1
                $html .= '    <table border="1" cellpadding="1" id="sheet' . $sheetIndex . '" cellspacing="1" style="' . $style . '">' . PHP_EOL;
1147
            } else {
1148 3
                $html .= '    <table border="0" cellpadding="1" id="sheet' . $sheetIndex . '" cellspacing="0" style="' . $style . '">' . PHP_EOL;
1149
            }
1150
        }
1151
1152
        // Write <col> elements
1153 6
        $highestColumnIndex = Cell::columnIndexFromString($pSheet->getHighestColumn()) - 1;
1154 6
        $i = -1;
1155 6
        while ($i++ < $highestColumnIndex) {
1156 6 View Code Duplication
            if (!$this->isPdf) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1157 3
                if (!$this->useInlineCss) {
1158 3
                    $html .= '        <col class="col' . $i . '">' . PHP_EOL;
1159
                } else {
1160
                    $style = isset($this->cssStyles['table.sheet' . $sheetIndex . ' col.col' . $i]) ?
1161
                        $this->assembleCSS($this->cssStyles['table.sheet' . $sheetIndex . ' col.col' . $i]) : '';
1162
                    $html .= '        <col style="' . $style . '">' . PHP_EOL;
1163
                }
1164
            }
1165
        }
1166
1167 6
        return $html;
1168
    }
1169
1170
    /**
1171
     * Generate table footer.
1172
     *
1173
     * @throws WriterException
1174
     */
1175 6
    private function generateTableFooter()
1176
    {
1177 6
        $html = '    </table>' . PHP_EOL;
1178
1179 6
        return $html;
1180
    }
1181
1182
    /**
1183
     * Generate row.
1184
     *
1185
     * @param Worksheet $pSheet \PhpOffice\PhpSpreadsheet\Worksheet
1186
     * @param array $pValues Array containing cells in a row
1187
     * @param int $pRow Row number (0-based)
1188
     * @param mixed $cellType eg: 'td'
1189
     *
1190
     * @throws WriterException
1191
     *
1192
     * @return string
1193
     */
1194 6
    private function generateRow(Worksheet $pSheet, array $pValues, $pRow, $cellType)
1195
    {
1196
        // Construct HTML
1197 6
        $html = '';
1198
1199
        // Sheet index
1200 6
        $sheetIndex = $pSheet->getParent()->getIndex($pSheet);
1201
1202
        // DomPDF and breaks
1203 6
        if ($this->isPdf && count($pSheet->getBreaks()) > 0) {
1204
            $breaks = $pSheet->getBreaks();
1205
1206
            // check if a break is needed before this row
1207
            if (isset($breaks['A' . $pRow])) {
1208
                // close table: </table>
1209
                $html .= $this->generateTableFooter();
1210
1211
                // insert page break
1212
                $html .= '<div style="page-break-before:always" />';
1213
1214
                // open table again: <table> + <col> etc.
1215
                $html .= $this->generateTableHeader($pSheet);
1216
            }
1217
        }
1218
1219
        // Write row start
1220 6 View Code Duplication
        if (!$this->useInlineCss) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1221 3
            $html .= '          <tr class="row' . $pRow . '">' . PHP_EOL;
1222
        } else {
1223 4
            $style = isset($this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $pRow])
1224 4
                ? $this->assembleCSS($this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $pRow]) : '';
1225
1226 4
            $html .= '          <tr style="' . $style . '">' . PHP_EOL;
1227
        }
1228
1229
        // Write cells
1230 6
        $colNum = 0;
1231 6
        foreach ($pValues as $cellAddress) {
1232 6
            $cell = ($cellAddress > '') ? $pSheet->getCell($cellAddress) : '';
1233 6
            $coordinate = Cell::stringFromColumnIndex($colNum) . ($pRow + 1);
1234 6
            if (!$this->useInlineCss) {
1235 3
                $cssClass = 'column' . $colNum;
1236
            } else {
1237 4
                $cssClass = [];
1238 4
                if ($cellType == 'th') {
1239 View Code Duplication
                    if (isset($this->cssStyles['table.sheet' . $sheetIndex . ' th.column' . $colNum])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1240
                        $this->cssStyles['table.sheet' . $sheetIndex . ' th.column' . $colNum];
1241
                    }
1242 View Code Duplication
                } else {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1243 4
                    if (isset($this->cssStyles['table.sheet' . $sheetIndex . ' td.column' . $colNum])) {
1244
                        $this->cssStyles['table.sheet' . $sheetIndex . ' td.column' . $colNum];
1245
                    }
1246
                }
1247
            }
1248 6
            $colSpan = 1;
1249 6
            $rowSpan = 1;
1250
1251
            // initialize
1252 6
            $cellData = '&nbsp;';
1253
1254
            // Cell
1255 6
            if ($cell instanceof Cell) {
1256 6
                $cellData = '';
1257 6
                if (is_null($cell->getParent())) {
1258
                    $cell->attach($pSheet);
0 ignored issues
show
Documentation introduced by
$pSheet is of type object<PhpOffice\PhpSpreadsheet\Worksheet>, but the function expects a object<PhpOffice\PhpSpreadsheet\Collection\Cells>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1259
                }
1260
                // Value
1261 6
                if ($cell->getValue() instanceof RichText) {
1262
                    // Loop through rich text elements
1263 5
                    $elements = $cell->getValue()->getRichTextElements();
1264 5
                    foreach ($elements as $element) {
1265
                        // Rich text start?
1266 5
                        if ($element instanceof RichText\Run) {
1267 5
                            $cellData .= '<span style="' . $this->assembleCSS($this->createCSSStyleFont($element->getFont())) . '">';
1268
1269 5
                            if ($element->getFont()->getSuperScript()) {
1270
                                $cellData .= '<sup>';
1271 5
                            } elseif ($element->getFont()->getSubScript()) {
1272
                                $cellData .= '<sub>';
1273
                            }
1274
                        }
1275
1276
                        // Convert UTF8 data to PCDATA
1277 5
                        $cellText = $element->getText();
1278 5
                        $cellData .= htmlspecialchars($cellText);
1279
1280 5
                        if ($element instanceof RichText\Run) {
1281 5
                            if ($element->getFont()->getSuperScript()) {
1282
                                $cellData .= '</sup>';
1283 5
                            } elseif ($element->getFont()->getSubScript()) {
1284
                                $cellData .= '</sub>';
1285
                            }
1286
1287 5
                            $cellData .= '</span>';
1288
                        }
1289
                    }
1290
                } else {
1291 6
                    if ($this->preCalculateFormulas) {
1292 6
                        $cellData = NumberFormat::toFormattedString(
1293 6
                            $cell->getCalculatedValue(),
1294 6
                            $pSheet->getParent()->getCellXfByIndex($cell->getXfIndex())->getNumberFormat()->getFormatCode(),
1295 6
                            [$this, 'formatColor']
1296
                        );
1297
                    } else {
1298
                        $cellData = NumberFormat::toFormattedString(
1299
                            $cell->getValue(),
1300
                            $pSheet->getParent()->getCellXfByIndex($cell->getXfIndex())->getNumberFormat()->getFormatCode(),
1301
                            [$this, 'formatColor']
1302
                        );
1303
                    }
1304 6
                    $cellData = htmlspecialchars($cellData);
1305 6
                    if ($pSheet->getParent()->getCellXfByIndex($cell->getXfIndex())->getFont()->getSuperScript()) {
1306
                        $cellData = '<sup>' . $cellData . '</sup>';
1307 6
                    } elseif ($pSheet->getParent()->getCellXfByIndex($cell->getXfIndex())->getFont()->getSubScript()) {
1308
                        $cellData = '<sub>' . $cellData . '</sub>';
1309
                    }
1310
                }
1311
1312
                // Converts the cell content so that spaces occuring at beginning of each new line are replaced by &nbsp;
1313
                // Example: "  Hello\n to the world" is converted to "&nbsp;&nbsp;Hello\n&nbsp;to the world"
1314 6
                $cellData = preg_replace('/(?m)(?:^|\\G) /', '&nbsp;', $cellData);
1315
1316
                // convert newline "\n" to '<br>'
1317 6
                $cellData = nl2br($cellData);
1318
1319
                // Extend CSS class?
1320 6
                if (!$this->useInlineCss) {
1321 3
                    $cssClass .= ' style' . $cell->getXfIndex();
1322 3
                    $cssClass .= ' ' . $cell->getDataType();
1323
                } else {
1324 4
                    if ($cellType == 'th') {
1325 View Code Duplication
                        if (isset($this->cssStyles['th.style' . $cell->getXfIndex()])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1326
                            $cssClass = array_merge($cssClass, $this->cssStyles['th.style' . $cell->getXfIndex()]);
1327
                        }
1328 View Code Duplication
                    } else {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1329 4
                        if (isset($this->cssStyles['td.style' . $cell->getXfIndex()])) {
1330 4
                            $cssClass = array_merge($cssClass, $this->cssStyles['td.style' . $cell->getXfIndex()]);
1331
                        }
1332
                    }
1333
1334
                    // General horizontal alignment: Actual horizontal alignment depends on dataType
1335 4
                    $sharedStyle = $pSheet->getParent()->getCellXfByIndex($cell->getXfIndex());
1336 4
                    if ($sharedStyle->getAlignment()->getHorizontal() == Alignment::HORIZONTAL_GENERAL
1337 4
                        && isset($this->cssStyles['.' . $cell->getDataType()]['text-align'])
1338
                    ) {
1339 4
                        $cssClass['text-align'] = $this->cssStyles['.' . $cell->getDataType()]['text-align'];
1340
                    }
1341
                }
1342
            }
1343
1344
            // Hyperlink?
1345 6
            if ($pSheet->hyperlinkExists($coordinate) && !$pSheet->getHyperlink($coordinate)->isInternal()) {
1346 4
                $cellData = '<a href="' . htmlspecialchars($pSheet->getHyperlink($coordinate)->getUrl()) . '" title="' . htmlspecialchars($pSheet->getHyperlink($coordinate)->getTooltip()) . '">' . $cellData . '</a>';
1347
            }
1348
1349
            // Should the cell be written or is it swallowed by a rowspan or colspan?
1350 6
            $writeCell = !(isset($this->isSpannedCell[$pSheet->getParent()->getIndex($pSheet)][$pRow + 1][$colNum])
1351 6
                && $this->isSpannedCell[$pSheet->getParent()->getIndex($pSheet)][$pRow + 1][$colNum]);
1352
1353
            // Colspan and Rowspan
1354 6
            $colspan = 1;
0 ignored issues
show
Unused Code introduced by
$colspan is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1355 6
            $rowspan = 1;
0 ignored issues
show
Unused Code introduced by
$rowspan is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1356 6
            if (isset($this->isBaseCell[$pSheet->getParent()->getIndex($pSheet)][$pRow + 1][$colNum])) {
1357 5
                $spans = $this->isBaseCell[$pSheet->getParent()->getIndex($pSheet)][$pRow + 1][$colNum];
1358 5
                $rowSpan = $spans['rowspan'];
1359 5
                $colSpan = $spans['colspan'];
1360
1361
                //    Also apply style from last cell in merge to fix borders -
1362
                //        relies on !important for non-none border declarations in createCSSStyleBorder
1363 5
                $endCellCoord = Cell::stringFromColumnIndex($colNum + $colSpan - 1) . ($pRow + $rowSpan);
1364 5
                if (!$this->useInlineCss) {
1365 2
                    $cssClass .= ' style' . $pSheet->getCell($endCellCoord)->getXfIndex();
1366
                }
1367
            }
1368
1369
            // Write
1370 6
            if ($writeCell) {
1371
                // Column start
1372 6
                $html .= '            <' . $cellType;
1373 6
                if (!$this->useInlineCss) {
1374 3
                    $html .= ' class="' . $cssClass . '"';
1375
                } else {
1376
                    //** Necessary redundant code for the sake of \PhpOffice\PhpSpreadsheet\Writer\Pdf **
1377
                    // We must explicitly write the width of the <td> element because TCPDF
1378
                    // does not recognize e.g. <col style="width:42pt">
1379 4
                    $width = 0;
1380 4
                    $i = $colNum - 1;
1381 4
                    $e = $colNum + $colSpan - 1;
1382 4
                    while ($i++ < $e) {
1383 4
                        if (isset($this->columnWidths[$sheetIndex][$i])) {
1384 4
                            $width += $this->columnWidths[$sheetIndex][$i];
1385
                        }
1386
                    }
1387 4
                    $cssClass['width'] = $width . 'pt';
1388
1389
                    // We must also explicitly write the height of the <td> element because TCPDF
1390
                    // does not recognize e.g. <tr style="height:50pt">
1391 4
                    if (isset($this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $pRow]['height'])) {
1392 1
                        $height = $this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $pRow]['height'];
1393 1
                        $cssClass['height'] = $height;
1394
                    }
1395
                    //** end of redundant code **
1396
1397 4
                    $html .= ' style="' . $this->assembleCSS($cssClass) . '"';
1398
                }
1399 6
                if ($colSpan > 1) {
1400 5
                    $html .= ' colspan="' . $colSpan . '"';
1401
                }
1402 6
                if ($rowSpan > 1) {
1403
                    $html .= ' rowspan="' . $rowSpan . '"';
1404
                }
1405 6
                $html .= '>';
1406
1407
                // Image?
1408 6
                $html .= $this->writeImageInCell($pSheet, $coordinate);
1409
1410
                // Chart?
1411 6
                if ($this->includeCharts) {
1412
                    $html .= $this->writeChartInCell($pSheet, $coordinate);
1413
                }
1414
1415
                // Cell data
1416 6
                $html .= $cellData;
1417
1418
                // Column end
1419 6
                $html .= '</' . $cellType . '>' . PHP_EOL;
1420
            }
1421
1422
            // Next column
1423 6
            ++$colNum;
1424
        }
1425
1426
        // Write row end
1427 6
        $html .= '          </tr>' . PHP_EOL;
1428
1429
        // Return
1430 6
        return $html;
1431
    }
1432
1433
    /**
1434
     * Takes array where of CSS properties / values and converts to CSS string.
1435
     *
1436
     * @param array
1437
     * @param mixed $pValue
1438
     *
1439
     * @return string
1440
     */
1441 6
    private function assembleCSS($pValue = [])
1442
    {
1443 6
        $pairs = [];
1444 6
        foreach ($pValue as $property => $value) {
1445 6
            $pairs[] = $property . ':' . $value;
1446
        }
1447 6
        $string = implode('; ', $pairs);
1448
1449 6
        return $string;
1450
    }
1451
1452
    /**
1453
     * Get images root.
1454
     *
1455
     * @return string
1456
     */
1457 4
    public function getImagesRoot()
1458
    {
1459 4
        return $this->imagesRoot;
1460
    }
1461
1462
    /**
1463
     * Set images root.
1464
     *
1465
     * @param string $pValue
1466
     *
1467
     * @return HTML
1468
     */
1469
    public function setImagesRoot($pValue)
1470
    {
1471
        $this->imagesRoot = $pValue;
1472
1473
        return $this;
1474
    }
1475
1476
    /**
1477
     * Get embed images.
1478
     *
1479
     * @return bool
1480
     */
1481
    public function getEmbedImages()
1482
    {
1483
        return $this->embedImages;
1484
    }
1485
1486
    /**
1487
     * Set embed images.
1488
     *
1489
     * @param bool $pValue
1490
     *
1491
     * @return HTML
1492
     */
1493
    public function setEmbedImages($pValue)
1494
    {
1495
        $this->embedImages = $pValue;
1496
1497
        return $this;
1498
    }
1499
1500
    /**
1501
     * Get use inline CSS?
1502
     *
1503
     * @return bool
1504
     */
1505
    public function getUseInlineCss()
1506
    {
1507
        return $this->useInlineCss;
1508
    }
1509
1510
    /**
1511
     * Set use inline CSS?
1512
     *
1513
     * @param bool $pValue
1514
     *
1515
     * @return HTML
1516
     */
1517 4
    public function setUseInlineCss($pValue)
1518
    {
1519 4
        $this->useInlineCss = $pValue;
1520
1521 4
        return $this;
1522
    }
1523
1524
    /**
1525
     * Add color to formatted string as inline style.
1526
     *
1527
     * @param string $pValue Plain formatted value without color
1528
     * @param string $pFormat Format code
1529
     *
1530
     * @return string
1531
     */
1532 4
    public function formatColor($pValue, $pFormat)
1533
    {
1534
        // Color information, e.g. [Red] is always at the beginning
1535 4
        $color = null; // initialize
1536 4
        $matches = [];
1537
1538 4
        $color_regex = '/^\\[[a-zA-Z]+\\]/';
1539 4
        if (preg_match($color_regex, $pFormat, $matches)) {
1540
            $color = str_replace(['[', ']'], '', $matches[0]);
1541
            $color = strtolower($color);
1542
        }
1543
1544
        // convert to PCDATA
1545 4
        $value = htmlspecialchars($pValue);
1546
1547
        // color span tag
1548 4
        if ($color !== null) {
1549
            $value = '<span style="color:' . $color . '">' . $value . '</span>';
1550
        }
1551
1552 4
        return $value;
1553
    }
1554
1555
    /**
1556
     * Calculate information about HTML colspan and rowspan which is not always the same as Excel's.
1557
     */
1558 6
    private function calculateSpans()
1559
    {
1560
        // Identify all cells that should be omitted in HTML due to cell merge.
1561
        // In HTML only the upper-left cell should be written and it should have
1562
        //   appropriate rowspan / colspan attribute
1563 6
        $sheetIndexes = $this->sheetIndex !== null ?
1564 6
            [$this->sheetIndex] : range(0, $this->spreadsheet->getSheetCount() - 1);
1565
1566 6
        foreach ($sheetIndexes as $sheetIndex) {
1567 6
            $sheet = $this->spreadsheet->getSheet($sheetIndex);
1568
1569 6
            $candidateSpannedRow = [];
1570
1571
            // loop through all Excel merged cells
1572 6
            foreach ($sheet->getMergeCells() as $cells) {
1573 5
                list($cells) = Cell::splitRange($cells);
0 ignored issues
show
Documentation introduced by
$cells is of type array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1574 5
                $first = $cells[0];
1575 5
                $last = $cells[1];
1576
1577 5
                list($fc, $fr) = Cell::coordinateFromString($first);
1578 5
                $fc = Cell::columnIndexFromString($fc) - 1;
1579
1580 5
                list($lc, $lr) = Cell::coordinateFromString($last);
1581 5
                $lc = Cell::columnIndexFromString($lc) - 1;
1582
1583
                // loop through the individual cells in the individual merge
1584 5
                $r = $fr - 1;
1585 5
                while ($r++ < $lr) {
1586
                    // also, flag this row as a HTML row that is candidate to be omitted
1587 5
                    $candidateSpannedRow[$r] = $r;
1588
1589 5
                    $c = $fc - 1;
1590 5
                    while ($c++ < $lc) {
1591 5
                        if (!($c == $fc && $r == $fr)) {
1592
                            // not the upper-left cell (should not be written in HTML)
1593 5
                            $this->isSpannedCell[$sheetIndex][$r][$c] = [
1594 5
                                'baseCell' => [$fr, $fc],
1595
                            ];
1596
                        } else {
1597
                            // upper-left is the base cell that should hold the colspan/rowspan attribute
1598 5
                            $this->isBaseCell[$sheetIndex][$r][$c] = [
1599 5
                                'xlrowspan' => $lr - $fr + 1, // Excel rowspan
1600 5
                                'rowspan' => $lr - $fr + 1, // HTML rowspan, value may change
1601 5
                                'xlcolspan' => $lc - $fc + 1, // Excel colspan
1602 5
                                'colspan' => $lc - $fc + 1, // HTML colspan, value may change
1603
                            ];
1604
                        }
1605
                    }
1606
                }
1607
            }
1608
1609
            // Identify which rows should be omitted in HTML. These are the rows where all the cells
1610
            //   participate in a merge and the where base cells are somewhere above.
1611 6
            $countColumns = Cell::columnIndexFromString($sheet->getHighestColumn());
1612 6
            foreach ($candidateSpannedRow as $rowIndex) {
1613 5
                if (isset($this->isSpannedCell[$sheetIndex][$rowIndex])) {
1614 5
                    if (count($this->isSpannedCell[$sheetIndex][$rowIndex]) == $countColumns) {
1615 5
                        $this->isSpannedRow[$sheetIndex][$rowIndex] = $rowIndex;
1616
                    }
1617
                }
1618
            }
1619
1620
            // For each of the omitted rows we found above, the affected rowspans should be subtracted by 1
1621 6
            if (isset($this->isSpannedRow[$sheetIndex])) {
1622 4
                foreach ($this->isSpannedRow[$sheetIndex] as $rowIndex) {
1623 4
                    $adjustedBaseCells = [];
1624 4
                    $c = -1;
1625 4
                    $e = $countColumns - 1;
1626 6
                    while ($c++ < $e) {
1627 4
                        $baseCell = $this->isSpannedCell[$sheetIndex][$rowIndex][$c]['baseCell'];
1628
1629 4
                        if (!in_array($baseCell, $adjustedBaseCells)) {
1630
                            // subtract rowspan by 1
1631 4
                            --$this->isBaseCell[$sheetIndex][$baseCell[0]][$baseCell[1]]['rowspan'];
1632 4
                            $adjustedBaseCells[] = $baseCell;
1633
                        }
1634
                    }
1635
                }
1636
            }
1637
1638
            // TODO: Same for columns
1639
        }
1640
1641
        // We have calculated the spans
1642 6
        $this->spansAreCalculated = true;
1643 6
    }
1644
1645 6
    private function setMargins(Worksheet $pSheet)
1646
    {
1647 6
        $htmlPage = '@page { ';
1648 6
        $htmlBody = 'body { ';
1649
1650 6
        $left = StringHelper::formatNumber($pSheet->getPageMargins()->getLeft()) . 'in; ';
1651 6
        $htmlPage .= 'margin-left: ' . $left;
1652 6
        $htmlBody .= 'margin-left: ' . $left;
1653 6
        $right = StringHelper::formatNumber($pSheet->getPageMargins()->getRight()) . 'in; ';
1654 6
        $htmlPage .= 'margin-right: ' . $right;
1655 6
        $htmlBody .= 'margin-right: ' . $right;
1656 6
        $top = StringHelper::formatNumber($pSheet->getPageMargins()->getTop()) . 'in; ';
1657 6
        $htmlPage .= 'margin-top: ' . $top;
1658 6
        $htmlBody .= 'margin-top: ' . $top;
1659 6
        $bottom = StringHelper::formatNumber($pSheet->getPageMargins()->getBottom()) . 'in; ';
1660 6
        $htmlPage .= 'margin-bottom: ' . $bottom;
1661 6
        $htmlBody .= 'margin-bottom: ' . $bottom;
1662
1663 6
        $htmlPage .= "}\n";
1664 6
        $htmlBody .= "}\n";
1665
1666 6
        return "<style>\n" . $htmlPage . $htmlBody . "</style>\n";
1667
    }
1668
}
1669