Failed Conditions
Pull Request — master (#1694)
by Adrien
08:05
created

Html::generateRowCellDataValue()   B

Complexity

Conditions 7
Paths 25

Size

Total Lines 22
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 7

Importance

Changes 0
Metric Value
cc 7
eloc 16
nc 25
nop 3
dl 0
loc 22
ccs 16
cts 16
cp 1
crap 7
rs 8.8333
c 0
b 0
f 0
1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Writer;
4
5
use HTMLPurifier;
6
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
7
use PhpOffice\PhpSpreadsheet\Cell\Cell;
8
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
9
use PhpOffice\PhpSpreadsheet\Chart\Chart;
10
use PhpOffice\PhpSpreadsheet\RichText\RichText;
11
use PhpOffice\PhpSpreadsheet\RichText\Run;
12
use PhpOffice\PhpSpreadsheet\Settings;
13
use PhpOffice\PhpSpreadsheet\Shared\Drawing as SharedDrawing;
14
use PhpOffice\PhpSpreadsheet\Shared\File;
15
use PhpOffice\PhpSpreadsheet\Shared\Font as SharedFont;
16
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
17
use PhpOffice\PhpSpreadsheet\Spreadsheet;
18
use PhpOffice\PhpSpreadsheet\Style\Alignment;
19
use PhpOffice\PhpSpreadsheet\Style\Border;
20
use PhpOffice\PhpSpreadsheet\Style\Borders;
21
use PhpOffice\PhpSpreadsheet\Style\Fill;
22
use PhpOffice\PhpSpreadsheet\Style\Font;
23
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
24
use PhpOffice\PhpSpreadsheet\Style\Style;
25
use PhpOffice\PhpSpreadsheet\Worksheet\Drawing;
26
use PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing;
27
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
28
29
class Html extends BaseWriter
30
{
31
    /**
32
     * Spreadsheet object.
33
     *
34
     * @var Spreadsheet
35
     */
36
    protected $spreadsheet;
37
38
    /**
39
     * Sheet index to write.
40
     *
41
     * @var null|int
42
     */
43
    private $sheetIndex = 0;
44
45
    /**
46
     * Images root.
47
     *
48
     * @var string
49
     */
50
    private $imagesRoot = '';
51
52
    /**
53
     * embed images, or link to images.
54
     *
55
     * @var bool
56
     */
57
    private $embedImages = false;
58
59
    /**
60
     * Use inline CSS?
61
     *
62
     * @var bool
63
     */
64
    private $useInlineCss = false;
65
66
    /**
67
     * Use embedded CSS?
68
     *
69
     * @var bool
70
     */
71
    private $useEmbeddedCSS = true;
72
73
    /**
74
     * Array of CSS styles.
75
     *
76
     * @var array
77
     */
78
    private $cssStyles;
79
80
    /**
81
     * Array of column widths in points.
82
     *
83
     * @var array
84
     */
85
    private $columnWidths;
86
87
    /**
88
     * Default font.
89
     *
90
     * @var Font
91
     */
92
    private $defaultFont;
93
94
    /**
95
     * Flag whether spans have been calculated.
96
     *
97
     * @var bool
98
     */
99
    private $spansAreCalculated = false;
100
101
    /**
102
     * Excel cells that should not be written as HTML cells.
103
     *
104
     * @var array
105
     */
106
    private $isSpannedCell = [];
107
108
    /**
109
     * Excel cells that are upper-left corner in a cell merge.
110
     *
111
     * @var array
112
     */
113
    private $isBaseCell = [];
114
115
    /**
116
     * Excel rows that should not be written as HTML rows.
117
     *
118
     * @var array
119
     */
120
    private $isSpannedRow = [];
121
122
    /**
123
     * Is the current writer creating PDF?
124
     *
125
     * @var bool
126
     */
127
    protected $isPdf = false;
128
129
    /**
130
     * Generate the Navigation block.
131
     *
132
     * @var bool
133
     */
134
    private $generateSheetNavigationBlock = true;
135
136
    /**
137
     * Callback for editing generated html.
138
     *
139
     * @var null|callable
140
     */
141
    private $editHtmlCallback;
142
143
    /**
144
     * Create a new HTML.
145
     */
146 185
    public function __construct(Spreadsheet $spreadsheet)
147
    {
148 185
        $this->spreadsheet = $spreadsheet;
149 185
        $this->defaultFont = $this->spreadsheet->getDefaultStyle()->getFont();
150 185
    }
151
152
    /**
153
     * Save Spreadsheet to file.
154
     *
155
     * @param resource|string $filename
156
     */
157 160
    public function save($filename, int $flags = 0): void
158
    {
159 160
        $this->processFlags($flags);
160
161
        // Open file
162 160
        $this->openFileHandle($filename);
163
164
        // Write html
165 159
        fwrite($this->fileHandle, $this->generateHTMLAll());
166
167
        // Close file
168 159
        $this->maybeCloseFileHandle();
169 159
    }
170
171
    /**
172
     * Save Spreadsheet as html to variable.
173
     *
174
     * @return string
175
     */
176 176
    public function generateHtmlAll()
177
    {
178
        // garbage collect
179 176
        $this->spreadsheet->garbageCollect();
180
181 176
        $saveDebugLog = Calculation::getInstance($this->spreadsheet)->getDebugLog()->getWriteDebugLog();
182 176
        Calculation::getInstance($this->spreadsheet)->getDebugLog()->setWriteDebugLog(false);
183 176
        $saveArrayReturnType = Calculation::getArrayReturnType();
184 176
        Calculation::setArrayReturnType(Calculation::RETURN_ARRAY_AS_VALUE);
185
186
        // Build CSS
187 176
        $this->buildCSS(!$this->useInlineCss);
188
189 176
        $html = '';
190
191
        // Write headers
192 176
        $html .= $this->generateHTMLHeader(!$this->useInlineCss);
193
194
        // Write navigation (tabs)
195 176
        if ((!$this->isPdf) && ($this->generateSheetNavigationBlock)) {
196 166
            $html .= $this->generateNavigation();
197
        }
198
199
        // Write data
200 176
        $html .= $this->generateSheetData();
201
202
        // Write footer
203 176
        $html .= $this->generateHTMLFooter();
204 176
        $callback = $this->editHtmlCallback;
205 176
        if ($callback) {
206 4
            $html = $callback($html);
207
        }
208
209 176
        Calculation::setArrayReturnType($saveArrayReturnType);
210 176
        Calculation::getInstance($this->spreadsheet)->getDebugLog()->setWriteDebugLog($saveDebugLog);
211
212 176
        return $html;
213
    }
214
215
    /**
216
     * Set a callback to edit the entire HTML.
217
     *
218
     * The callback must accept the HTML as string as first parameter,
219
     * and it must return the edited HTML as string.
220
     */
221 4
    public function setEditHtmlCallback(?callable $callback): void
222
    {
223 4
        $this->editHtmlCallback = $callback;
224 4
    }
225
226
    const VALIGN_ARR = [
227
        Alignment::VERTICAL_BOTTOM => 'bottom',
228
        Alignment::VERTICAL_TOP => 'top',
229
        Alignment::VERTICAL_CENTER => 'middle',
230
        Alignment::VERTICAL_JUSTIFY => 'middle',
231
    ];
232
233
    /**
234
     * Map VAlign.
235
     *
236
     * @param string $vAlign Vertical alignment
237
     *
238
     * @return string
239
     */
240 176
    private function mapVAlign($vAlign)
241
    {
242 176
        return array_key_exists($vAlign, self::VALIGN_ARR) ? self::VALIGN_ARR[$vAlign] : 'baseline';
243
    }
244
245
    const HALIGN_ARR = [
246
        Alignment::HORIZONTAL_LEFT => 'left',
247
        Alignment::HORIZONTAL_RIGHT => 'right',
248
        Alignment::HORIZONTAL_CENTER => 'center',
249
        Alignment::HORIZONTAL_CENTER_CONTINUOUS => 'center',
250
        Alignment::HORIZONTAL_JUSTIFY => 'justify',
251
    ];
252
253
    /**
254
     * Map HAlign.
255
     *
256
     * @param string $hAlign Horizontal alignment
257
     *
258
     * @return string
259
     */
260 176
    private function mapHAlign($hAlign)
261
    {
262 176
        return array_key_exists($hAlign, self::HALIGN_ARR) ? self::HALIGN_ARR[$hAlign] : '';
263
    }
264
265
    const BORDER_ARR = [
266
        Border::BORDER_NONE => 'none',
267
        Border::BORDER_DASHDOT => '1px dashed',
268
        Border::BORDER_DASHDOTDOT => '1px dotted',
269
        Border::BORDER_DASHED => '1px dashed',
270
        Border::BORDER_DOTTED => '1px dotted',
271
        Border::BORDER_DOUBLE => '3px double',
272
        Border::BORDER_HAIR => '1px solid',
273
        Border::BORDER_MEDIUM => '2px solid',
274
        Border::BORDER_MEDIUMDASHDOT => '2px dashed',
275
        Border::BORDER_MEDIUMDASHDOTDOT => '2px dotted',
276
        Border::BORDER_SLANTDASHDOT => '2px dashed',
277
        Border::BORDER_THICK => '3px solid',
278
    ];
279
280
    /**
281
     * Map border style.
282
     *
283
     * @param int $borderStyle Sheet index
284
     *
285
     * @return string
286
     */
287 176
    private function mapBorderStyle($borderStyle)
288
    {
289 176
        return array_key_exists($borderStyle, self::BORDER_ARR) ? self::BORDER_ARR[$borderStyle] : '1px solid';
290
    }
291
292
    /**
293
     * Get sheet index.
294
     *
295
     * @return int
296
     */
297 9
    public function getSheetIndex()
298
    {
299 9
        return $this->sheetIndex;
300
    }
301
302
    /**
303
     * Set sheet index.
304
     *
305
     * @param int $pValue Sheet index
306
     *
307
     * @return $this
308
     */
309 1
    public function setSheetIndex($pValue)
310
    {
311 1
        $this->sheetIndex = $pValue;
312
313 1
        return $this;
314
    }
315
316
    /**
317
     * Get sheet index.
318
     *
319
     * @return bool
320
     */
321 1
    public function getGenerateSheetNavigationBlock()
322
    {
323 1
        return $this->generateSheetNavigationBlock;
324
    }
325
326
    /**
327
     * Set sheet index.
328
     *
329
     * @param bool $pValue Flag indicating whether the sheet navigation block should be generated or not
330
     *
331
     * @return $this
332
     */
333 1
    public function setGenerateSheetNavigationBlock($pValue)
334
    {
335 1
        $this->generateSheetNavigationBlock = (bool) $pValue;
336
337 1
        return $this;
338
    }
339
340
    /**
341
     * Write all sheets (resets sheetIndex to NULL).
342
     *
343
     * @return $this
344
     */
345 7
    public function writeAllSheets()
346
    {
347 7
        $this->sheetIndex = null;
348
349 7
        return $this;
350
    }
351
352 176
    private static function generateMeta($val, $desc)
353
    {
354 176
        return $val
355 176
            ? ('      <meta name="' . $desc . '" content="' . htmlspecialchars($val, Settings::htmlEntityFlags()) . '" />' . PHP_EOL)
356 176
            : '';
357
    }
358
359
    /**
360
     * Generate HTML header.
361
     *
362
     * @param bool $pIncludeStyles Include styles?
363
     *
364
     * @return string
365
     */
366 176
    public function generateHTMLHeader($pIncludeStyles = false)
367
    {
368
        // Construct HTML
369 176
        $properties = $this->spreadsheet->getProperties();
370 176
        $html = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">' . PHP_EOL;
371 176
        $html .= '<html xmlns="http://www.w3.org/1999/xhtml">' . PHP_EOL;
372 176
        $html .= '  <head>' . PHP_EOL;
373 176
        $html .= '      <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />' . PHP_EOL;
374 176
        $html .= '      <meta name="generator" content="PhpSpreadsheet, https://github.com/PHPOffice/PhpSpreadsheet" />' . PHP_EOL;
375 176
        $html .= '      <title>' . htmlspecialchars($properties->getTitle(), Settings::htmlEntityFlags()) . '</title>' . PHP_EOL;
376 176
        $html .= self::generateMeta($properties->getCreator(), 'author');
377 176
        $html .= self::generateMeta($properties->getTitle(), 'title');
378 176
        $html .= self::generateMeta($properties->getDescription(), 'description');
379 176
        $html .= self::generateMeta($properties->getSubject(), 'subject');
380 176
        $html .= self::generateMeta($properties->getKeywords(), 'keywords');
381 176
        $html .= self::generateMeta($properties->getCategory(), 'category');
382 176
        $html .= self::generateMeta($properties->getCompany(), 'company');
383 176
        $html .= self::generateMeta($properties->getManager(), 'manager');
384
385 176
        $html .= $pIncludeStyles ? $this->generateStyles(true) : $this->generatePageDeclarations(true);
386
387 176
        $html .= '  </head>' . PHP_EOL;
388 176
        $html .= '' . PHP_EOL;
389 176
        $html .= '  <body>' . PHP_EOL;
390
391 176
        return $html;
392
    }
393
394 176
    private function generateSheetPrep()
395
    {
396
        // Ensure that Spans have been calculated?
397 176
        $this->calculateSpans();
398
399
        // Fetch sheets
400 176
        if ($this->sheetIndex === null) {
401 7
            $sheets = $this->spreadsheet->getAllSheets();
402
        } else {
403 174
            $sheets = [$this->spreadsheet->getSheet($this->sheetIndex)];
404
        }
405
406 176
        return $sheets;
407
    }
408
409 176
    private function generateSheetStarts($sheet, $rowMin)
410
    {
411
        // calculate start of <tbody>, <thead>
412 176
        $tbodyStart = $rowMin;
413 176
        $theadStart = $theadEnd = 0; // default: no <thead>    no </thead>
414 176
        if ($sheet->getPageSetup()->isRowsToRepeatAtTopSet()) {
415 2
            $rowsToRepeatAtTop = $sheet->getPageSetup()->getRowsToRepeatAtTop();
416
417
            // we can only support repeating rows that start at top row
418 2
            if ($rowsToRepeatAtTop[0] == 1) {
419 2
                $theadStart = $rowsToRepeatAtTop[0];
420 2
                $theadEnd = $rowsToRepeatAtTop[1];
421 2
                $tbodyStart = $rowsToRepeatAtTop[1] + 1;
422
            }
423
        }
424
425 176
        return [$theadStart, $theadEnd, $tbodyStart];
426
    }
427
428 176
    private function generateSheetTags($row, $theadStart, $theadEnd, $tbodyStart)
429
    {
430
        // <thead> ?
431 176
        $startTag = ($row == $theadStart) ? ('        <thead>' . PHP_EOL) : '';
432 176
        if (!$startTag) {
433 176
            $startTag = ($row == $tbodyStart) ? ('        <tbody>' . PHP_EOL) : '';
434
        }
435 176
        $endTag = ($row == $theadEnd) ? ('        </thead>' . PHP_EOL) : '';
436 176
        $cellType = ($row >= $tbodyStart) ? 'td' : 'th';
437
438 176
        return [$cellType, $startTag, $endTag];
439
    }
440
441
    /**
442
     * Generate sheet data.
443
     *
444
     * @return string
445
     */
446 176
    public function generateSheetData()
447
    {
448 176
        $sheets = $this->generateSheetPrep();
449
450
        // Construct HTML
451 176
        $html = '';
452
453
        // Loop all sheets
454 176
        $sheetId = 0;
455 176
        foreach ($sheets as $sheet) {
456
            // Write table header
457 176
            $html .= $this->generateTableHeader($sheet);
458
459
            // Get worksheet dimension
460 176
            [$min, $max] = explode(':', $sheet->calculateWorksheetDataDimension());
461 176
            [$minCol, $minRow] = Coordinate::indexesFromString($min);
462 176
            [$maxCol, $maxRow] = Coordinate::indexesFromString($max);
463
464 176
            [$theadStart, $theadEnd, $tbodyStart] = $this->generateSheetStarts($sheet, $minRow);
465
466
            // Loop through cells
467 176
            $row = $minRow - 1;
468 176
            while ($row++ < $maxRow) {
469 176
                [$cellType, $startTag, $endTag] = $this->generateSheetTags($row, $theadStart, $theadEnd, $tbodyStart);
470 176
                $html .= $startTag;
471
472
                // Write row if there are HTML table cells in it
473 176
                if (!isset($this->isSpannedRow[$sheet->getParent()->getIndex($sheet)][$row])) {
474
                    // Start a new rowData
475 176
                    $rowData = [];
476
                    // Loop through columns
477 176
                    $column = $minCol;
478 176
                    while ($column <= $maxCol) {
479
                        // Cell exists?
480 176
                        if ($sheet->cellExistsByColumnAndRow($column, $row)) {
481 175
                            $rowData[$column] = Coordinate::stringFromColumnIndex($column) . $row;
482
                        } else {
483 20
                            $rowData[$column] = '';
484
                        }
485 176
                        ++$column;
486
                    }
487 176
                    $html .= $this->generateRow($sheet, $rowData, $row - 1, $cellType);
488
                }
489
490 176
                $html .= $endTag;
491
            }
492 176
            $html .= $this->extendRowsForChartsAndImages($sheet, $row);
493
494
            // Write table footer
495 176
            $html .= $this->generateTableFooter();
496
            // Writing PDF?
497 176
            if ($this->isPdf && $this->useInlineCss) {
498 4
                if ($this->sheetIndex === null && $sheetId + 1 < $this->spreadsheet->getSheetCount()) {
499 1
                    $html .= '<div style="page-break-before:always" ></div>';
500
                }
501
            }
502
503
            // Next sheet
504 176
            ++$sheetId;
505
        }
506
507 176
        return $html;
508
    }
509
510
    /**
511
     * Generate sheet tabs.
512
     *
513
     * @return string
514
     */
515 166
    public function generateNavigation()
516
    {
517
        // Fetch sheets
518 166
        $sheets = [];
519 166
        if ($this->sheetIndex === null) {
520 4
            $sheets = $this->spreadsheet->getAllSheets();
521
        } else {
522 166
            $sheets[] = $this->spreadsheet->getSheet($this->sheetIndex);
523
        }
524
525
        // Construct HTML
526 166
        $html = '';
527
528
        // Only if there are more than 1 sheets
529 166
        if (count($sheets) > 1) {
530
            // Loop all sheets
531 4
            $sheetId = 0;
532
533 4
            $html .= '<ul class="navigation">' . PHP_EOL;
534
535 4
            foreach ($sheets as $sheet) {
536 4
                $html .= '  <li class="sheet' . $sheetId . '"><a href="#sheet' . $sheetId . '">' . $sheet->getTitle() . '</a></li>' . PHP_EOL;
537 4
                ++$sheetId;
538
            }
539
540 4
            $html .= '</ul>' . PHP_EOL;
541
        }
542
543 166
        return $html;
544
    }
545
546
    /**
547
     * Extend Row if chart is placed after nominal end of row.
548
     * This code should be exercised by sample:
549
     * Chart/32_Chart_read_write_PDF.php.
550
     * However, that test is suppressed due to out-of-date
551
     * Jpgraph code issuing warnings. So, don't measure
552
     * code coverage for this function till that is fixed.
553
     *
554
     * @param int $row Row to check for charts
555
     *
556
     * @return array
557
     *
558
     * @codeCoverageIgnore
559
     */
560
    private function extendRowsForCharts(Worksheet $worksheet, int $row)
561
    {
562
        $rowMax = $row;
563
        $colMax = 'A';
564
        $anyfound = false;
565
        if ($this->includeCharts) {
566
            foreach ($worksheet->getChartCollection() as $chart) {
567
                if ($chart instanceof Chart) {
568
                    $anyfound = true;
569
                    $chartCoordinates = $chart->getTopLeftPosition();
570
                    $chartTL = Coordinate::coordinateFromString($chartCoordinates['cell']);
571
                    $chartCol = Coordinate::columnIndexFromString($chartTL[0]);
572
                    if ($chartTL[1] > $rowMax) {
573
                        $rowMax = $chartTL[1];
574
                        if ($chartCol > Coordinate::columnIndexFromString($colMax)) {
575
                            $colMax = $chartTL[0];
576
                        }
577
                    }
578
                }
579
            }
580
        }
581
582
        return [$rowMax, $colMax, $anyfound];
583
    }
584
585 176
    private function extendRowsForChartsAndImages(Worksheet $worksheet, int $row): string
586
    {
587 176
        [$rowMax, $colMax, $anyfound] = $this->extendRowsForCharts($worksheet, $row);
588
589 176
        foreach ($worksheet->getDrawingCollection() as $drawing) {
590 12
            $anyfound = true;
591 12
            $imageTL = Coordinate::coordinateFromString($drawing->getCoordinates());
592 12
            $imageCol = Coordinate::columnIndexFromString($imageTL[0]);
593 12
            if ($imageTL[1] > $rowMax) {
594 1
                $rowMax = $imageTL[1];
595 1
                if ($imageCol > Coordinate::columnIndexFromString($colMax)) {
596 1
                    $colMax = $imageTL[0];
597
                }
598
            }
599
        }
600
601
        // Don't extend rows if not needed
602 176
        if ($row === $rowMax || !$anyfound) {
603 175
            return '';
604
        }
605
606 1
        $html = '';
607 1
        ++$colMax;
608 1
        ++$row;
609 1
        while ($row <= $rowMax) {
610 1
            $html .= '<tr>';
611 1
            for ($col = 'A'; $col != $colMax; ++$col) {
612 1
                $htmlx = $this->writeImageInCell($worksheet, $col . $row);
613 1
                $htmlx .= $this->includeCharts ? $this->writeChartInCell($worksheet, $col . $row) : '';
614 1
                if ($htmlx) {
615 1
                    $html .= "<td class='style0' style='position: relative;'>$htmlx</td>";
616
                } else {
617 1
                    $html .= "<td class='style0'></td>";
618
                }
619
            }
620 1
            ++$row;
621 1
            $html .= '</tr>' . PHP_EOL;
622
        }
623
624 1
        return $html;
625
    }
626
627
    /**
628
     * Convert Windows file name to file protocol URL.
629
     *
630
     * @param string $filename file name on local system
631
     *
632
     * @return string
633
     */
634 12
    public static function winFileToUrl($filename)
635
    {
636
        // Windows filename
637 12
        if (substr($filename, 1, 2) === ':\\') {
638 1
            $filename = 'file:///' . str_replace('\\', '/', $filename);
639
        }
640
641 12
        return $filename;
642
    }
643
644
    /**
645
     * Generate image tag in cell.
646
     *
647
     * @param Worksheet $worksheet \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet
648
     * @param string $coordinates Cell coordinates
649
     *
650
     * @return string
651
     */
652 176
    private function writeImageInCell(Worksheet $worksheet, $coordinates)
653
    {
654
        // Construct HTML
655 176
        $html = '';
656
657
        // Write images
658 176
        foreach ($worksheet->getDrawingCollection() as $drawing) {
659 12
            if ($drawing->getCoordinates() != $coordinates) {
660 12
                continue;
661
            }
662 12
            $filedesc = $drawing->getDescription();
663 12
            $filedesc = $filedesc ? htmlspecialchars($filedesc, ENT_QUOTES) : 'Embedded image';
664 12
            if ($drawing instanceof Drawing) {
665 11
                $filename = $drawing->getPath();
666
667
                // Strip off eventual '.'
668 11
                $filename = preg_replace('/^[.]/', '', $filename);
669
670
                // Prepend images root
671 11
                $filename = $this->getImagesRoot() . $filename;
672
673
                // Strip off eventual '.' if followed by non-/
674 11
                $filename = preg_replace('@^[.]([^/])@', '$1', $filename);
675
676
                // Convert UTF8 data to PCDATA
677 11
                $filename = htmlspecialchars($filename, Settings::htmlEntityFlags());
678
679 11
                $html .= PHP_EOL;
680 11
                $imageData = self::winFileToUrl($filename);
681
682 11
                if ($this->embedImages && !$this->isPdf) {
683 2
                    $picture = @file_get_contents($filename);
684 2
                    if ($picture !== false) {
685 2
                        $imageDetails = getimagesize($filename);
686
                        // base64 encode the binary data
687 2
                        $base64 = base64_encode($picture);
688 2
                        $imageData = 'data:' . $imageDetails['mime'] . ';base64,' . $base64;
689
                    }
690
                }
691
692
                $html .= '<img style="position: absolute; z-index: 1; left: ' .
693 11
                    $drawing->getOffsetX() . 'px; top: ' . $drawing->getOffsetY() . 'px; width: ' .
694 11
                    $drawing->getWidth() . 'px; height: ' . $drawing->getHeight() . 'px;" src="' .
695 11
                    $imageData . '" alt="' . $filedesc . '" />';
696 1
            } elseif ($drawing instanceof MemoryDrawing) {
697 1
                $imageResource = $drawing->getImageResource();
698 1
                if ($imageResource) {
699 1
                    ob_start(); //  Let's start output buffering.
700 1
                    imagepng($imageResource); //  This will normally output the image, but because of ob_start(), it won't.
701 1
                    $contents = ob_get_contents(); //  Instead, output above is saved to $contents
702 1
                    ob_end_clean(); //  End the output buffer.
703
704 1
                    $dataUri = 'data:image/jpeg;base64,' . base64_encode($contents);
705
706
                    //  Because of the nature of tables, width is more important than height.
707
                    //  max-width: 100% ensures that image doesnt overflow containing cell
708
                    //  width: X sets width of supplied image.
709
                    //  As a result, images bigger than cell will be contained and images smaller will not get stretched
710 1
                    $html .= '<img alt="' . $filedesc . '" src="' . $dataUri . '" style="max-width:100%;width:' . $drawing->getWidth() . 'px;" />';
711
                }
712
            }
713
        }
714
715 176
        return $html;
716
    }
717
718
    /**
719
     * Generate chart tag in cell.
720
     * This code should be exercised by sample:
721
     * Chart/32_Chart_read_write_PDF.php.
722
     * However, that test is suppressed due to out-of-date
723
     * Jpgraph code issuing warnings. So, don't measure
724
     * code coverage for this function till that is fixed.
725
     *
726
     * @codeCoverageIgnore
727
     */
728
    private function writeChartInCell(Worksheet $worksheet, string $coordinates): string
729
    {
730
        // Construct HTML
731
        $html = '';
732
733
        // Write charts
734
        foreach ($worksheet->getChartCollection() as $chart) {
735
            if ($chart instanceof Chart) {
736
                $chartCoordinates = $chart->getTopLeftPosition();
737
                if ($chartCoordinates['cell'] == $coordinates) {
738
                    $chartFileName = File::sysGetTempDir() . '/' . uniqid('', true) . '.png';
739
                    if (!$chart->render($chartFileName)) {
740
                        return '';
741
                    }
742
743
                    $html .= PHP_EOL;
744
                    $imageDetails = getimagesize($chartFileName);
745
                    $filedesc = $chart->getTitle();
746
                    $filedesc = $filedesc ? $filedesc->getCaptionText() : '';
747
                    $filedesc = $filedesc ? htmlspecialchars($filedesc, ENT_QUOTES) : 'Embedded chart';
748
                    if ($fp = fopen($chartFileName, 'rb', 0)) {
749
                        $picture = fread($fp, filesize($chartFileName));
750
                        fclose($fp);
751
                        // base64 encode the binary data
752
                        $base64 = base64_encode($picture);
753
                        $imageData = 'data:' . $imageDetails['mime'] . ';base64,' . $base64;
754
755
                        $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 . '" alt="' . $filedesc . '" />' . PHP_EOL;
756
757
                        unlink($chartFileName);
758
                    }
759
                }
760
            }
761
        }
762
763
        // Return
764
        return $html;
765
    }
766
767
    /**
768
     * Generate CSS styles.
769
     *
770
     * @param bool $generateSurroundingHTML Generate surrounding HTML tags? (&lt;style&gt; and &lt;/style&gt;)
771
     *
772
     * @return string
773
     */
774 173
    public function generateStyles($generateSurroundingHTML = true)
775
    {
776
        // Build CSS
777 173
        $css = $this->buildCSS($generateSurroundingHTML);
778
779
        // Construct HTML
780 173
        $html = '';
781
782
        // Start styles
783 173
        if ($generateSurroundingHTML) {
784 173
            $html .= '    <style type="text/css">' . PHP_EOL;
785 173
            $html .= (array_key_exists('html', $css)) ? ('      html { ' . $this->assembleCSS($css['html']) . ' }' . PHP_EOL) : '';
786
        }
787
788
        // Write all other styles
789 173
        foreach ($css as $styleName => $styleDefinition) {
790 173
            if ($styleName != 'html') {
791 173
                $html .= '      ' . $styleName . ' { ' . $this->assembleCSS($styleDefinition) . ' }' . PHP_EOL;
792
            }
793
        }
794 173
        $html .= $this->generatePageDeclarations(false);
795
796
        // End styles
797 173
        if ($generateSurroundingHTML) {
798 173
            $html .= '    </style>' . PHP_EOL;
799
        }
800
801
        // Return
802 173
        return $html;
803
    }
804
805 176
    private function buildCssRowHeights(Worksheet $sheet, array &$css, int $sheetIndex): void
806
    {
807
        // Calculate row heights
808 176
        foreach ($sheet->getRowDimensions() as $rowDimension) {
809 6
            $row = $rowDimension->getRowIndex() - 1;
810
811
            // table.sheetN tr.rowYYYYYY { }
812 6
            $css['table.sheet' . $sheetIndex . ' tr.row' . $row] = [];
813
814 6
            if ($rowDimension->getRowHeight() != -1) {
815 2
                $pt_height = $rowDimension->getRowHeight();
816 2
                $css['table.sheet' . $sheetIndex . ' tr.row' . $row]['height'] = $pt_height . 'pt';
817
            }
818 6
            if ($rowDimension->getVisible() === false) {
819 1
                $css['table.sheet' . $sheetIndex . ' tr.row' . $row]['display'] = 'none';
820 1
                $css['table.sheet' . $sheetIndex . ' tr.row' . $row]['visibility'] = 'hidden';
821
            }
822
        }
823 176
    }
824
825 176
    private function buildCssPerSheet(Worksheet $sheet, array &$css): void
826
    {
827
        // Calculate hash code
828 176
        $sheetIndex = $sheet->getParent()->getIndex($sheet);
829
830
        // Build styles
831
        // Calculate column widths
832 176
        $sheet->calculateColumnWidths();
833
834
        // col elements, initialize
835 176
        $highestColumnIndex = Coordinate::columnIndexFromString($sheet->getHighestColumn()) - 1;
836 176
        $column = -1;
837 176
        while ($column++ < $highestColumnIndex) {
838 176
            $this->columnWidths[$sheetIndex][$column] = 42; // approximation
839 176
            $css['table.sheet' . $sheetIndex . ' col.col' . $column]['width'] = '42pt';
840
        }
841
842
        // col elements, loop through columnDimensions and set width
843 176
        foreach ($sheet->getColumnDimensions() as $columnDimension) {
844 15
            $column = Coordinate::columnIndexFromString($columnDimension->getColumnIndex()) - 1;
845 15
            $width = SharedDrawing::cellDimensionToPixels($columnDimension->getWidth(), $this->defaultFont);
846 15
            $width = SharedDrawing::pixelsToPoints($width);
847 15
            if ($columnDimension->getVisible() === false) {
848 2
                $css['table.sheet' . $sheetIndex . ' .column' . $column]['display'] = 'none';
849
            }
850 15
            if ($width >= 0) {
851 12
                $this->columnWidths[$sheetIndex][$column] = $width;
852 12
                $css['table.sheet' . $sheetIndex . ' col.col' . $column]['width'] = $width . 'pt';
853
            }
854
        }
855
856
        // Default row height
857 176
        $rowDimension = $sheet->getDefaultRowDimension();
858
859
        // table.sheetN tr { }
860 176
        $css['table.sheet' . $sheetIndex . ' tr'] = [];
861
862 176
        if ($rowDimension->getRowHeight() == -1) {
863 175
            $pt_height = SharedFont::getDefaultRowHeightByFont($this->spreadsheet->getDefaultStyle()->getFont());
864
        } else {
865 1
            $pt_height = $rowDimension->getRowHeight();
866
        }
867 176
        $css['table.sheet' . $sheetIndex . ' tr']['height'] = $pt_height . 'pt';
868 176
        if ($rowDimension->getVisible() === false) {
869 1
            $css['table.sheet' . $sheetIndex . ' tr']['display'] = 'none';
870 1
            $css['table.sheet' . $sheetIndex . ' tr']['visibility'] = 'hidden';
871
        }
872
873 176
        $this->buildCssRowHeights($sheet, $css, $sheetIndex);
874 176
    }
875
876
    /**
877
     * Build CSS styles.
878
     *
879
     * @param bool $generateSurroundingHTML Generate surrounding HTML style? (html { })
880
     *
881
     * @return array
882
     */
883 176
    public function buildCSS($generateSurroundingHTML = true)
884
    {
885
        // Cached?
886 176
        if ($this->cssStyles !== null) {
887 173
            return $this->cssStyles;
888
        }
889
890
        // Ensure that spans have been calculated
891 176
        $this->calculateSpans();
892
893
        // Construct CSS
894 176
        $css = [];
895
896
        // Start styles
897 176
        if ($generateSurroundingHTML) {
898
            // html { }
899 173
            $css['html']['font-family'] = 'Calibri, Arial, Helvetica, sans-serif';
900 173
            $css['html']['font-size'] = '11pt';
901 173
            $css['html']['background-color'] = 'white';
902
        }
903
904
        // CSS for comments as found in LibreOffice
905 176
        $css['a.comment-indicator:hover + div.comment'] = [
906
            'background' => '#ffd',
907
            'position' => 'absolute',
908
            'display' => 'block',
909
            'border' => '1px solid black',
910
            'padding' => '0.5em',
911
        ];
912
913 176
        $css['a.comment-indicator'] = [
914
            'background' => 'red',
915
            'display' => 'inline-block',
916
            'border' => '1px solid black',
917
            'width' => '0.5em',
918
            'height' => '0.5em',
919
        ];
920
921 176
        $css['div.comment']['display'] = 'none';
922
923
        // table { }
924 176
        $css['table']['border-collapse'] = 'collapse';
925
926
        // .b {}
927 176
        $css['.b']['text-align'] = 'center'; // BOOL
928
929
        // .e {}
930 176
        $css['.e']['text-align'] = 'center'; // ERROR
931
932
        // .f {}
933 176
        $css['.f']['text-align'] = 'right'; // FORMULA
934
935
        // .inlineStr {}
936 176
        $css['.inlineStr']['text-align'] = 'left'; // INLINE
937
938
        // .n {}
939 176
        $css['.n']['text-align'] = 'right'; // NUMERIC
940
941
        // .s {}
942 176
        $css['.s']['text-align'] = 'left'; // STRING
943
944
        // Calculate cell style hashes
945 176
        foreach ($this->spreadsheet->getCellXfCollection() as $index => $style) {
946 176
            $css['td.style' . $index] = $this->createCSSStyle($style);
947 176
            $css['th.style' . $index] = $this->createCSSStyle($style);
948
        }
949
950
        // Fetch sheets
951 176
        $sheets = [];
952 176
        if ($this->sheetIndex === null) {
953 7
            $sheets = $this->spreadsheet->getAllSheets();
954
        } else {
955 174
            $sheets[] = $this->spreadsheet->getSheet($this->sheetIndex);
956
        }
957
958
        // Build styles per sheet
959 176
        foreach ($sheets as $sheet) {
960 176
            $this->buildCssPerSheet($sheet, $css);
961
        }
962
963
        // Cache
964 176
        if ($this->cssStyles === null) {
965 176
            $this->cssStyles = $css;
966
        }
967
968
        // Return
969 176
        return $css;
970
    }
971
972
    /**
973
     * Create CSS style.
974
     *
975
     * @return array
976
     */
977 176
    private function createCSSStyle(Style $pStyle)
978
    {
979
        // Create CSS
980 176
        return array_merge(
981 176
            $this->createCSSStyleAlignment($pStyle->getAlignment()),
982 176
            $this->createCSSStyleBorders($pStyle->getBorders()),
983 176
            $this->createCSSStyleFont($pStyle->getFont()),
984 176
            $this->createCSSStyleFill($pStyle->getFill())
985
        );
986
    }
987
988
    /**
989
     * Create CSS style (\PhpOffice\PhpSpreadsheet\Style\Alignment).
990
     *
991
     * @param Alignment $pStyle \PhpOffice\PhpSpreadsheet\Style\Alignment
992
     *
993
     * @return array
994
     */
995 176
    private function createCSSStyleAlignment(Alignment $pStyle)
996
    {
997
        // Construct CSS
998 176
        $css = [];
999
1000
        // Create CSS
1001 176
        $css['vertical-align'] = $this->mapVAlign($pStyle->getVertical());
1002 176
        $textAlign = $this->mapHAlign($pStyle->getHorizontal());
1003 176
        if ($textAlign) {
1004 9
            $css['text-align'] = $textAlign;
1005 9
            if (in_array($textAlign, ['left', 'right'])) {
1006 8
                $css['padding-' . $textAlign] = (string) ((int) $pStyle->getIndent() * 9) . 'px';
1007
            }
1008
        }
1009
1010 176
        return $css;
1011
    }
1012
1013
    /**
1014
     * Create CSS style (\PhpOffice\PhpSpreadsheet\Style\Font).
1015
     *
1016
     * @return array
1017
     */
1018 176
    private function createCSSStyleFont(Font $pStyle)
1019
    {
1020
        // Construct CSS
1021 176
        $css = [];
1022
1023
        // Create CSS
1024 176
        if ($pStyle->getBold()) {
1025 9
            $css['font-weight'] = 'bold';
1026
        }
1027 176
        if ($pStyle->getUnderline() != Font::UNDERLINE_NONE && $pStyle->getStrikethrough()) {
1028 1
            $css['text-decoration'] = 'underline line-through';
1029 176
        } elseif ($pStyle->getUnderline() != Font::UNDERLINE_NONE) {
1030 9
            $css['text-decoration'] = 'underline';
1031 176
        } elseif ($pStyle->getStrikethrough()) {
1032 1
            $css['text-decoration'] = 'line-through';
1033
        }
1034 176
        if ($pStyle->getItalic()) {
1035 8
            $css['font-style'] = 'italic';
1036
        }
1037
1038 176
        $css['color'] = '#' . $pStyle->getColor()->getRGB();
1039 176
        $css['font-family'] = '\'' . $pStyle->getName() . '\'';
1040 176
        $css['font-size'] = $pStyle->getSize() . 'pt';
1041
1042 176
        return $css;
1043
    }
1044
1045
    /**
1046
     * Create CSS style (Borders).
1047
     *
1048
     * @param Borders $pStyle Borders
1049
     *
1050
     * @return array
1051
     */
1052 176
    private function createCSSStyleBorders(Borders $pStyle)
1053
    {
1054
        // Construct CSS
1055 176
        $css = [];
1056
1057
        // Create CSS
1058 176
        $css['border-bottom'] = $this->createCSSStyleBorder($pStyle->getBottom());
1059 176
        $css['border-top'] = $this->createCSSStyleBorder($pStyle->getTop());
1060 176
        $css['border-left'] = $this->createCSSStyleBorder($pStyle->getLeft());
1061 176
        $css['border-right'] = $this->createCSSStyleBorder($pStyle->getRight());
1062
1063 176
        return $css;
1064
    }
1065
1066
    /**
1067
     * Create CSS style (Border).
1068
     *
1069
     * @param Border $pStyle Border
1070
     *
1071
     * @return string
1072
     */
1073 176
    private function createCSSStyleBorder(Border $pStyle)
1074
    {
1075
        //    Create CSS - add !important to non-none border styles for merged cells
1076 176
        $borderStyle = $this->mapBorderStyle($pStyle->getBorderStyle());
0 ignored issues
show
Bug introduced by
$pStyle->getBorderStyle() of type string is incompatible with the type integer expected by parameter $borderStyle of PhpOffice\PhpSpreadsheet...\Html::mapBorderStyle(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1076
        $borderStyle = $this->mapBorderStyle(/** @scrutinizer ignore-type */ $pStyle->getBorderStyle());
Loading history...
1077
1078 176
        return $borderStyle . ' #' . $pStyle->getColor()->getRGB() . (($borderStyle == 'none') ? '' : ' !important');
1079
    }
1080
1081
    /**
1082
     * Create CSS style (Fill).
1083
     *
1084
     * @param Fill $pStyle Fill
1085
     *
1086
     * @return array
1087
     */
1088 176
    private function createCSSStyleFill(Fill $pStyle)
1089
    {
1090
        // Construct HTML
1091 176
        $css = [];
1092
1093
        // Create CSS
1094 176
        $value = $pStyle->getFillType() == Fill::FILL_NONE ?
1095 176
            'white' : '#' . $pStyle->getStartColor()->getRGB();
1096 176
        $css['background-color'] = $value;
1097
1098 176
        return $css;
1099
    }
1100
1101
    /**
1102
     * Generate HTML footer.
1103
     */
1104 176
    public function generateHTMLFooter()
1105
    {
1106
        // Construct HTML
1107 176
        $html = '';
1108 176
        $html .= '  </body>' . PHP_EOL;
1109 176
        $html .= '</html>' . PHP_EOL;
1110
1111 176
        return $html;
1112
    }
1113
1114 6
    private function generateTableTagInline(Worksheet $worksheet, $id)
1115
    {
1116 6
        $style = isset($this->cssStyles['table']) ?
1117 6
            $this->assembleCSS($this->cssStyles['table']) : '';
1118
1119 6
        $prntgrid = $worksheet->getPrintGridlines();
1120 6
        $viewgrid = $this->isPdf ? $prntgrid : $worksheet->getShowGridlines();
1121 6
        if ($viewgrid && $prntgrid) {
1122 1
            $html = "    <table border='1' cellpadding='1' $id cellspacing='1' style='$style' class='gridlines gridlinesp'>" . PHP_EOL;
1123 6
        } elseif ($viewgrid) {
1124 2
            $html = "    <table border='0' cellpadding='0' $id cellspacing='0' style='$style' class='gridlines'>" . PHP_EOL;
1125 5
        } elseif ($prntgrid) {
1126 1
            $html = "    <table border='0' cellpadding='0' $id cellspacing='0' style='$style' class='gridlinesp'>" . PHP_EOL;
1127
        } else {
1128 5
            $html = "    <table border='0' cellpadding='1' $id cellspacing='0' style='$style'>" . PHP_EOL;
1129
        }
1130
1131 6
        return $html;
1132
    }
1133
1134 176
    private function generateTableTag(Worksheet $worksheet, $id, &$html, $sheetIndex): void
1135
    {
1136 176
        if (!$this->useInlineCss) {
1137 173
            $gridlines = $worksheet->getShowGridlines() ? ' gridlines' : '';
1138 173
            $gridlinesp = $worksheet->getPrintGridlines() ? ' gridlinesp' : '';
1139 173
            $html .= "    <table border='0' cellpadding='0' cellspacing='0' $id class='sheet$sheetIndex$gridlines$gridlinesp'>" . PHP_EOL;
1140
        } else {
1141 6
            $html .= $this->generateTableTagInline($worksheet, $id);
1142
        }
1143 176
    }
1144
1145
    /**
1146
     * Generate table header.
1147
     *
1148
     * @param Worksheet $worksheet The worksheet for the table we are writing
1149
     * @param bool $showid whether or not to add id to table tag
1150
     *
1151
     * @return string
1152
     */
1153 176
    private function generateTableHeader(Worksheet $worksheet, $showid = true)
1154
    {
1155 176
        $sheetIndex = $worksheet->getParent()->getIndex($worksheet);
1156
1157
        // Construct HTML
1158 176
        $html = '';
1159 176
        $id = $showid ? "id='sheet$sheetIndex'" : '';
1160 176
        if ($showid) {
1161 176
            $html .= "<div style='page: page$sheetIndex'>\n";
1162
        } else {
1163 2
            $html .= "<div style='page: page$sheetIndex' class='scrpgbrk'>\n";
1164
        }
1165
1166 176
        $this->generateTableTag($worksheet, $id, $html, $sheetIndex);
1167
1168
        // Write <col> elements
1169 176
        $highestColumnIndex = Coordinate::columnIndexFromString($worksheet->getHighestColumn()) - 1;
1170 176
        $i = -1;
1171 176
        while ($i++ < $highestColumnIndex) {
1172 176
            if (!$this->useInlineCss) {
1173 173
                $html .= '        <col class="col' . $i . '" />' . PHP_EOL;
1174
            } else {
1175 6
                $style = isset($this->cssStyles['table.sheet' . $sheetIndex . ' col.col' . $i]) ?
1176 6
                    $this->assembleCSS($this->cssStyles['table.sheet' . $sheetIndex . ' col.col' . $i]) : '';
1177 6
                $html .= '        <col style="' . $style . '" />' . PHP_EOL;
1178
            }
1179
        }
1180
1181 176
        return $html;
1182
    }
1183
1184
    /**
1185
     * Generate table footer.
1186
     */
1187 176
    private function generateTableFooter()
1188
    {
1189 176
        return '    </tbody></table>' . PHP_EOL . '</div>' . PHP_EOL;
1190
    }
1191
1192
    /**
1193
     * Generate row start.
1194
     *
1195
     * @param int $sheetIndex Sheet index (0-based)
1196
     * @param int $pRow row number
1197
     *
1198
     * @return string
1199
     */
1200 176
    private function generateRowStart(Worksheet $worksheet, $sheetIndex, $pRow)
1201
    {
1202 176
        $html = '';
1203 176
        if (count($worksheet->getBreaks()) > 0) {
1204 2
            $breaks = $worksheet->getBreaks();
1205
1206
            // check if a break is needed before this row
1207 2
            if (isset($breaks['A' . $pRow])) {
1208
                // close table: </table>
1209 2
                $html .= $this->generateTableFooter();
1210 2
                if ($this->isPdf && $this->useInlineCss) {
1211 1
                    $html .= '<div style="page-break-before:always" />';
1212
                }
1213
1214
                // open table again: <table> + <col> etc.
1215 2
                $html .= $this->generateTableHeader($worksheet, false);
1216 2
                $html .= '<tbody>' . PHP_EOL;
1217
            }
1218
        }
1219
1220
        // Write row start
1221 176
        if (!$this->useInlineCss) {
1222 173
            $html .= '          <tr class="row' . $pRow . '">' . PHP_EOL;
1223
        } else {
1224 6
            $style = isset($this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $pRow])
1225 6
                ? $this->assembleCSS($this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $pRow]) : '';
1226
1227 6
            $html .= '          <tr style="' . $style . '">' . PHP_EOL;
1228
        }
1229
1230 176
        return $html;
1231
    }
1232
1233 176
    private function generateRowCellCss(Worksheet $worksheet, $cellAddress, $pRow, $colNum)
1234
    {
1235 176
        $cell = ($cellAddress > '') ? $worksheet->getCell($cellAddress) : '';
1236 176
        $coordinate = Coordinate::stringFromColumnIndex($colNum + 1) . ($pRow + 1);
1237 176
        if (!$this->useInlineCss) {
1238 173
            $cssClass = 'column' . $colNum;
1239
        } else {
1240 6
            $cssClass = [];
1241
            // The statements below do nothing.
1242
            // Commenting out the code rather than deleting it
1243
            // in case someone can figure out what their intent was.
1244
            //if ($cellType == 'th') {
1245
            //    if (isset($this->cssStyles['table.sheet' . $sheetIndex . ' th.column' . $colNum])) {
1246
            //        $this->cssStyles['table.sheet' . $sheetIndex . ' th.column' . $colNum];
1247
            //    }
1248
            //} else {
1249
            //    if (isset($this->cssStyles['table.sheet' . $sheetIndex . ' td.column' . $colNum])) {
1250
            //        $this->cssStyles['table.sheet' . $sheetIndex . ' td.column' . $colNum];
1251
            //    }
1252
            //}
1253
            // End of mystery statements.
1254
        }
1255
1256 176
        return [$cell, $cssClass, $coordinate];
1257
    }
1258
1259 10
    private function generateRowCellDataValueRich($cell, &$cellData): void
1260
    {
1261
        // Loop through rich text elements
1262 10
        $elements = $cell->getValue()->getRichTextElements();
1263 10
        foreach ($elements as $element) {
1264
            // Rich text start?
1265 10
            if ($element instanceof Run) {
1266 10
                $cellData .= '<span style="' . $this->assembleCSS($this->createCSSStyleFont($element->getFont())) . '">';
1267
1268 10
                $cellEnd = '';
1269 10
                if ($element->getFont()->getSuperscript()) {
1270 1
                    $cellData .= '<sup>';
1271 1
                    $cellEnd = '</sup>';
1272 10
                } elseif ($element->getFont()->getSubscript()) {
1273 1
                    $cellData .= '<sub>';
1274 1
                    $cellEnd = '</sub>';
1275
                }
1276
1277
                // Convert UTF8 data to PCDATA
1278 10
                $cellText = $element->getText();
1279 10
                $cellData .= htmlspecialchars($cellText, Settings::htmlEntityFlags());
1280
1281 10
                $cellData .= $cellEnd;
1282
1283 10
                $cellData .= '</span>';
1284
            } else {
1285
                // Convert UTF8 data to PCDATA
1286 9
                $cellText = $element->getText();
1287 9
                $cellData .= htmlspecialchars($cellText, Settings::htmlEntityFlags());
1288
            }
1289
        }
1290 10
    }
1291
1292 175
    private function generateRowCellDataValue(Worksheet $worksheet, $cell, &$cellData): void
1293
    {
1294 175
        if ($cell->getValue() instanceof RichText) {
1295 10
            $this->generateRowCellDataValueRich($cell, $cellData);
1296
        } else {
1297 175
            $origData = $this->preCalculateFormulas ? $cell->getCalculatedValue() : $cell->getValue();
1298 175
            $formatCode = $worksheet->getParent()->getCellXfByIndex($cell->getXfIndex())->getNumberFormat()->getFormatCode();
1299 175
            if ($formatCode !== null) {
1300 175
                $cellData = NumberFormat::toFormattedString(
1301 175
                    $origData,
1302 175
                    $formatCode,
1303 175
                    [$this, 'formatColor']
1304
                );
1305
            }
1306
1307 175
            if ($cellData === $origData) {
1308 57
                $cellData = htmlspecialchars($cellData ?? '', Settings::htmlEntityFlags());
1309
            }
1310 175
            if ($worksheet->getParent()->getCellXfByIndex($cell->getXfIndex())->getFont()->getSuperscript()) {
1311 1
                $cellData = '<sup>' . $cellData . '</sup>';
1312 175
            } elseif ($worksheet->getParent()->getCellXfByIndex($cell->getXfIndex())->getFont()->getSubscript()) {
1313 1
                $cellData = '<sub>' . $cellData . '</sub>';
1314
            }
1315
        }
1316 175
    }
1317
1318 176
    private function generateRowCellData(Worksheet $worksheet, $cell, &$cssClass, $cellType)
1319
    {
1320 176
        $cellData = '&nbsp;';
1321 176
        if ($cell instanceof Cell) {
1322 175
            $cellData = '';
1323
            // Don't know what this does, and no test cases.
1324
            //if ($cell->getParent() === null) {
1325
            //    $cell->attach($worksheet);
1326
            //}
1327
            // Value
1328 175
            $this->generateRowCellDataValue($worksheet, $cell, $cellData);
1329
1330
            // Converts the cell content so that spaces occuring at beginning of each new line are replaced by &nbsp;
1331
            // Example: "  Hello\n to the world" is converted to "&nbsp;&nbsp;Hello\n&nbsp;to the world"
1332 175
            $cellData = preg_replace('/(?m)(?:^|\\G) /', '&nbsp;', $cellData);
1333
1334
            // convert newline "\n" to '<br>'
1335 175
            $cellData = nl2br($cellData);
1336
1337
            // Extend CSS class?
1338 175
            if (!$this->useInlineCss) {
1339 172
                $cssClass .= ' style' . $cell->getXfIndex();
1340 172
                $cssClass .= ' ' . $cell->getDataType();
1341
            } else {
1342 6
                if ($cellType == 'th') {
1343 1
                    if (isset($this->cssStyles['th.style' . $cell->getXfIndex()])) {
1344 1
                        $cssClass = array_merge($cssClass, $this->cssStyles['th.style' . $cell->getXfIndex()]);
1345
                    }
1346
                } else {
1347 6
                    if (isset($this->cssStyles['td.style' . $cell->getXfIndex()])) {
1348 6
                        $cssClass = array_merge($cssClass, $this->cssStyles['td.style' . $cell->getXfIndex()]);
1349
                    }
1350
                }
1351
1352
                // General horizontal alignment: Actual horizontal alignment depends on dataType
1353 6
                $sharedStyle = $worksheet->getParent()->getCellXfByIndex($cell->getXfIndex());
1354
                if (
1355 6
                    $sharedStyle->getAlignment()->getHorizontal() == Alignment::HORIZONTAL_GENERAL
1356 6
                    && isset($this->cssStyles['.' . $cell->getDataType()]['text-align'])
1357
                ) {
1358 175
                    $cssClass['text-align'] = $this->cssStyles['.' . $cell->getDataType()]['text-align'];
1359
                }
1360
            }
1361
        } else {
1362
            // Use default borders for empty cell
1363 20
            if (is_string($cssClass)) {
1364 18
                $cssClass .= ' style0';
1365
            }
1366
        }
1367
1368 176
        return $cellData;
1369
    }
1370
1371 176
    private function generateRowIncludeCharts(Worksheet $worksheet, $coordinate)
1372
    {
1373 176
        return $this->includeCharts ? $this->writeChartInCell($worksheet, $coordinate) : '';
1374
    }
1375
1376 176
    private function generateRowSpans($html, $rowSpan, $colSpan)
1377
    {
1378 176
        $html .= ($colSpan > 1) ? (' colspan="' . $colSpan . '"') : '';
1379 176
        $html .= ($rowSpan > 1) ? (' rowspan="' . $rowSpan . '"') : '';
1380
1381 176
        return $html;
1382
    }
1383
1384 176
    private function generateRowWriteCell(&$html, Worksheet $worksheet, $coordinate, $cellType, $cellData, $colSpan, $rowSpan, $cssClass, $colNum, $sheetIndex, $pRow): void
1385
    {
1386
        // Image?
1387 176
        $htmlx = $this->writeImageInCell($worksheet, $coordinate);
1388
        // Chart?
1389 176
        $htmlx .= $this->generateRowIncludeCharts($worksheet, $coordinate);
1390
        // Column start
1391 176
        $html .= '            <' . $cellType;
1392 176
        if (!$this->useInlineCss && !$this->isPdf) {
1393 166
            $html .= ' class="' . $cssClass . '"';
1394 166
            if ($htmlx) {
1395 166
                $html .= " style='position: relative;'";
1396
            }
1397
        } else {
1398
            //** Necessary redundant code for the sake of \PhpOffice\PhpSpreadsheet\Writer\Pdf **
1399
            // We must explicitly write the width of the <td> element because TCPDF
1400
            // does not recognize e.g. <col style="width:42pt">
1401 13
            if ($this->useInlineCss) {
1402 6
                $xcssClass = $cssClass;
1403
            } else {
1404 8
                $html .= ' class="' . $cssClass . '"';
1405 8
                $xcssClass = [];
1406
            }
1407 13
            $width = 0;
1408 13
            $i = $colNum - 1;
1409 13
            $e = $colNum + $colSpan - 1;
1410 13
            while ($i++ < $e) {
1411 13
                if (isset($this->columnWidths[$sheetIndex][$i])) {
1412 13
                    $width += $this->columnWidths[$sheetIndex][$i];
1413
                }
1414
            }
1415 13
            $xcssClass['width'] = $width . 'pt';
1416
1417
            // We must also explicitly write the height of the <td> element because TCPDF
1418
            // does not recognize e.g. <tr style="height:50pt">
1419 13
            if (isset($this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $pRow]['height'])) {
1420 1
                $height = $this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $pRow]['height'];
1421 1
                $xcssClass['height'] = $height;
1422
            }
1423
            //** end of redundant code **
1424
1425 13
            if ($htmlx) {
1426 5
                $xcssClass['position'] = 'relative';
1427
            }
1428 13
            $html .= ' style="' . $this->assembleCSS($xcssClass) . '"';
1429
        }
1430 176
        $html = $this->generateRowSpans($html, $rowSpan, $colSpan);
1431
1432 176
        $html .= '>';
1433 176
        $html .= $htmlx;
1434
1435 176
        $html .= $this->writeComment($worksheet, $coordinate);
1436
1437
        // Cell data
1438 176
        $html .= $cellData;
1439
1440
        // Column end
1441 176
        $html .= '</' . $cellType . '>' . PHP_EOL;
1442 176
    }
1443
1444
    /**
1445
     * Generate row.
1446
     *
1447
     * @param array $pValues Array containing cells in a row
1448
     * @param int $pRow Row number (0-based)
1449
     * @param string $cellType eg: 'td'
1450
     *
1451
     * @return string
1452
     */
1453 176
    private function generateRow(Worksheet $worksheet, array $pValues, $pRow, $cellType)
1454
    {
1455
        // Sheet index
1456 176
        $sheetIndex = $worksheet->getParent()->getIndex($worksheet);
1457 176
        $html = $this->generateRowStart($worksheet, $sheetIndex, $pRow);
1458
1459
        // Write cells
1460 176
        $colNum = 0;
1461 176
        foreach ($pValues as $cellAddress) {
1462 176
            [$cell, $cssClass, $coordinate] = $this->generateRowCellCss($worksheet, $cellAddress, $pRow, $colNum);
1463
1464 176
            $colSpan = 1;
1465 176
            $rowSpan = 1;
1466
1467
            // Cell Data
1468 176
            $cellData = $this->generateRowCellData($worksheet, $cell, $cssClass, $cellType);
1469
1470
            // Hyperlink?
1471 176
            if ($worksheet->hyperlinkExists($coordinate) && !$worksheet->getHyperlink($coordinate)->isInternal()) {
1472 8
                $cellData = '<a href="' . htmlspecialchars($worksheet->getHyperlink($coordinate)->getUrl(), Settings::htmlEntityFlags()) . '" title="' . htmlspecialchars($worksheet->getHyperlink($coordinate)->getTooltip(), Settings::htmlEntityFlags()) . '">' . $cellData . '</a>';
1473
            }
1474
1475
            // Should the cell be written or is it swallowed by a rowspan or colspan?
1476 176
            $writeCell = !(isset($this->isSpannedCell[$worksheet->getParent()->getIndex($worksheet)][$pRow + 1][$colNum])
1477 176
                && $this->isSpannedCell[$worksheet->getParent()->getIndex($worksheet)][$pRow + 1][$colNum]);
1478
1479
            // Colspan and Rowspan
1480 176
            $colspan = 1;
0 ignored issues
show
Unused Code introduced by
The assignment to $colspan is dead and can be removed.
Loading history...
1481 176
            $rowspan = 1;
0 ignored issues
show
Unused Code introduced by
The assignment to $rowspan is dead and can be removed.
Loading history...
1482 176
            if (isset($this->isBaseCell[$worksheet->getParent()->getIndex($worksheet)][$pRow + 1][$colNum])) {
1483 10
                $spans = $this->isBaseCell[$worksheet->getParent()->getIndex($worksheet)][$pRow + 1][$colNum];
1484 10
                $rowSpan = $spans['rowspan'];
1485 10
                $colSpan = $spans['colspan'];
1486
1487
                //    Also apply style from last cell in merge to fix borders -
1488
                //        relies on !important for non-none border declarations in createCSSStyleBorder
1489 10
                $endCellCoord = Coordinate::stringFromColumnIndex($colNum + $colSpan) . ($pRow + $rowSpan);
1490 10
                if (!$this->useInlineCss) {
1491 9
                    $cssClass .= ' style' . $worksheet->getCell($endCellCoord)->getXfIndex();
1492
                }
1493
            }
1494
1495
            // Write
1496 176
            if ($writeCell) {
1497 176
                $this->generateRowWriteCell($html, $worksheet, $coordinate, $cellType, $cellData, $colSpan, $rowSpan, $cssClass, $colNum, $sheetIndex, $pRow);
1498
            }
1499
1500
            // Next column
1501 176
            ++$colNum;
1502
        }
1503
1504
        // Write row end
1505 176
        $html .= '          </tr>' . PHP_EOL;
1506
1507
        // Return
1508 176
        return $html;
1509
    }
1510
1511
    /**
1512
     * Takes array where of CSS properties / values and converts to CSS string.
1513
     *
1514
     * @return string
1515
     */
1516 176
    private function assembleCSS(array $pValue = [])
1517
    {
1518 176
        $pairs = [];
1519 176
        foreach ($pValue as $property => $value) {
1520 176
            $pairs[] = $property . ':' . $value;
1521
        }
1522 176
        $string = implode('; ', $pairs);
1523
1524 176
        return $string;
1525
    }
1526
1527
    /**
1528
     * Get images root.
1529
     *
1530
     * @return string
1531
     */
1532 11
    public function getImagesRoot()
1533
    {
1534 11
        return $this->imagesRoot;
1535
    }
1536
1537
    /**
1538
     * Set images root.
1539
     *
1540
     * @param string $pValue
1541
     *
1542
     * @return $this
1543
     */
1544 1
    public function setImagesRoot($pValue)
1545
    {
1546 1
        $this->imagesRoot = $pValue;
1547
1548 1
        return $this;
1549
    }
1550
1551
    /**
1552
     * Get embed images.
1553
     *
1554
     * @return bool
1555
     */
1556 1
    public function getEmbedImages()
1557
    {
1558 1
        return $this->embedImages;
1559
    }
1560
1561
    /**
1562
     * Set embed images.
1563
     *
1564
     * @param bool $pValue
1565
     *
1566
     * @return $this
1567
     */
1568 2
    public function setEmbedImages($pValue)
1569
    {
1570 2
        $this->embedImages = $pValue;
1571
1572 2
        return $this;
1573
    }
1574
1575
    /**
1576
     * Get use inline CSS?
1577
     *
1578
     * @return bool
1579
     */
1580 1
    public function getUseInlineCss()
1581
    {
1582 1
        return $this->useInlineCss;
1583
    }
1584
1585
    /**
1586
     * Set use inline CSS?
1587
     *
1588
     * @param bool $pValue
1589
     *
1590
     * @return $this
1591
     */
1592 7
    public function setUseInlineCss($pValue)
1593
    {
1594 7
        $this->useInlineCss = $pValue;
1595
1596 7
        return $this;
1597
    }
1598
1599
    /**
1600
     * Get use embedded CSS?
1601
     *
1602
     * @return bool
1603
     *
1604
     * @codeCoverageIgnore
1605
     *
1606
     * @deprecated no longer used
1607
     */
1608
    public function getUseEmbeddedCSS()
1609
    {
1610
        return $this->useEmbeddedCSS;
1611
    }
1612
1613
    /**
1614
     * Set use embedded CSS?
1615
     *
1616
     * @param bool $pValue
1617
     *
1618
     * @return $this
1619
     *
1620
     * @codeCoverageIgnore
1621
     *
1622
     * @deprecated no longer used
1623
     */
1624
    public function setUseEmbeddedCSS($pValue)
1625
    {
1626
        $this->useEmbeddedCSS = $pValue;
1627
1628
        return $this;
1629
    }
1630
1631
    /**
1632
     * Add color to formatted string as inline style.
1633
     *
1634
     * @param string $pValue Plain formatted value without color
1635
     * @param string $pFormat Format code
1636
     *
1637
     * @return string
1638
     */
1639 127
    public function formatColor($pValue, $pFormat)
1640
    {
1641
        // Color information, e.g. [Red] is always at the beginning
1642 127
        $color = null; // initialize
1643 127
        $matches = [];
1644
1645 127
        $color_regex = '/^\\[[a-zA-Z]+\\]/';
1646 127
        if (preg_match($color_regex, $pFormat, $matches)) {
1647 17
            $color = str_replace(['[', ']'], '', $matches[0]);
1648 17
            $color = strtolower($color);
1649
        }
1650
1651
        // convert to PCDATA
1652 127
        $value = htmlspecialchars($pValue, Settings::htmlEntityFlags());
1653
1654
        // color span tag
1655 127
        if ($color !== null) {
1656 17
            $value = '<span style="color:' . $color . '">' . $value . '</span>';
1657
        }
1658
1659 127
        return $value;
1660
    }
1661
1662
    /**
1663
     * Calculate information about HTML colspan and rowspan which is not always the same as Excel's.
1664
     */
1665 176
    private function calculateSpans(): void
1666
    {
1667 176
        if ($this->spansAreCalculated) {
1668 176
            return;
1669
        }
1670
        // Identify all cells that should be omitted in HTML due to cell merge.
1671
        // In HTML only the upper-left cell should be written and it should have
1672
        //   appropriate rowspan / colspan attribute
1673 176
        $sheetIndexes = $this->sheetIndex !== null ?
1674 176
            [$this->sheetIndex] : range(0, $this->spreadsheet->getSheetCount() - 1);
1675
1676 176
        foreach ($sheetIndexes as $sheetIndex) {
1677 176
            $sheet = $this->spreadsheet->getSheet($sheetIndex);
1678
1679 176
            $candidateSpannedRow = [];
1680
1681
            // loop through all Excel merged cells
1682 176
            foreach ($sheet->getMergeCells() as $cells) {
1683 10
                [$cells] = Coordinate::splitRange($cells);
1684 10
                $first = $cells[0];
1685 10
                $last = $cells[1];
1686
1687 10
                [$fc, $fr] = Coordinate::indexesFromString($first);
1688 10
                $fc = $fc - 1;
1689
1690 10
                [$lc, $lr] = Coordinate::indexesFromString($last);
1691 10
                $lc = $lc - 1;
1692
1693
                // loop through the individual cells in the individual merge
1694 10
                $r = $fr - 1;
1695 10
                while ($r++ < $lr) {
1696
                    // also, flag this row as a HTML row that is candidate to be omitted
1697 10
                    $candidateSpannedRow[$r] = $r;
1698
1699 10
                    $c = $fc - 1;
1700 10
                    while ($c++ < $lc) {
1701 10
                        if (!($c == $fc && $r == $fr)) {
1702
                            // not the upper-left cell (should not be written in HTML)
1703 10
                            $this->isSpannedCell[$sheetIndex][$r][$c] = [
1704 10
                                'baseCell' => [$fr, $fc],
1705
                            ];
1706
                        } else {
1707
                            // upper-left is the base cell that should hold the colspan/rowspan attribute
1708 10
                            $this->isBaseCell[$sheetIndex][$r][$c] = [
1709 10
                                'xlrowspan' => $lr - $fr + 1, // Excel rowspan
1710 10
                                'rowspan' => $lr - $fr + 1, // HTML rowspan, value may change
1711 10
                                'xlcolspan' => $lc - $fc + 1, // Excel colspan
1712 10
                                'colspan' => $lc - $fc + 1, // HTML colspan, value may change
1713
                            ];
1714
                        }
1715
                    }
1716
                }
1717
            }
1718
1719 176
            $this->calculateSpansOmitRows($sheet, $sheetIndex, $candidateSpannedRow);
1720
1721
            // TODO: Same for columns
1722
        }
1723
1724
        // We have calculated the spans
1725 176
        $this->spansAreCalculated = true;
1726 176
    }
1727
1728 176
    private function calculateSpansOmitRows($sheet, $sheetIndex, $candidateSpannedRow): void
1729
    {
1730
        // Identify which rows should be omitted in HTML. These are the rows where all the cells
1731
        //   participate in a merge and the where base cells are somewhere above.
1732 176
        $countColumns = Coordinate::columnIndexFromString($sheet->getHighestColumn());
1733 176
        foreach ($candidateSpannedRow as $rowIndex) {
1734 10
            if (isset($this->isSpannedCell[$sheetIndex][$rowIndex])) {
1735 10
                if (count($this->isSpannedCell[$sheetIndex][$rowIndex]) == $countColumns) {
1736 8
                    $this->isSpannedRow[$sheetIndex][$rowIndex] = $rowIndex;
1737
                }
1738
            }
1739
        }
1740
1741
        // For each of the omitted rows we found above, the affected rowspans should be subtracted by 1
1742 176
        if (isset($this->isSpannedRow[$sheetIndex])) {
1743 8
            foreach ($this->isSpannedRow[$sheetIndex] as $rowIndex) {
1744 8
                $adjustedBaseCells = [];
1745 8
                $c = -1;
1746 8
                $e = $countColumns - 1;
1747 8
                while ($c++ < $e) {
1748 8
                    $baseCell = $this->isSpannedCell[$sheetIndex][$rowIndex][$c]['baseCell'];
1749
1750 8
                    if (!in_array($baseCell, $adjustedBaseCells)) {
1751
                        // subtract rowspan by 1
1752 8
                        --$this->isBaseCell[$sheetIndex][$baseCell[0]][$baseCell[1]]['rowspan'];
1753 8
                        $adjustedBaseCells[] = $baseCell;
1754
                    }
1755
                }
1756
            }
1757
        }
1758 176
    }
1759
1760
    /**
1761
     * Write a comment in the same format as LibreOffice.
1762
     *
1763
     * @see https://github.com/LibreOffice/core/blob/9fc9bf3240f8c62ad7859947ab8a033ac1fe93fa/sc/source/filter/html/htmlexp.cxx#L1073-L1092
1764
     *
1765
     * @param string $coordinate
1766
     *
1767
     * @return string
1768
     */
1769 176
    private function writeComment(Worksheet $worksheet, $coordinate)
1770
    {
1771 176
        $result = '';
1772 176
        if (!$this->isPdf && isset($worksheet->getComments()[$coordinate])) {
1773 16
            $sanitizer = new HTMLPurifier();
1774 16
            $sanitizedString = $sanitizer->purify($worksheet->getComment($coordinate)->getText()->getPlainText());
1775 16
            if ($sanitizedString !== '') {
1776 14
                $result .= '<a class="comment-indicator"></a>';
1777 14
                $result .= '<div class="comment">' . nl2br($sanitizedString) . '</div>';
1778 14
                $result .= PHP_EOL;
1779
            }
1780
        }
1781
1782 176
        return $result;
1783
    }
1784
1785
    /**
1786
     * Generate @page declarations.
1787
     *
1788
     * @param bool $generateSurroundingHTML
1789
     *
1790
     * @return    string
1791
     */
1792 176
    private function generatePageDeclarations($generateSurroundingHTML)
1793
    {
1794
        // Ensure that Spans have been calculated?
1795 176
        $this->calculateSpans();
1796
1797
        // Fetch sheets
1798 176
        $sheets = [];
1799 176
        if ($this->sheetIndex === null) {
1800 7
            $sheets = $this->spreadsheet->getAllSheets();
1801
        } else {
1802 174
            $sheets[] = $this->spreadsheet->getSheet($this->sheetIndex);
1803
        }
1804
1805
        // Construct HTML
1806 176
        $htmlPage = $generateSurroundingHTML ? ('<style type="text/css">' . PHP_EOL) : '';
1807
1808
        // Loop all sheets
1809 176
        $sheetId = 0;
1810 176
        foreach ($sheets as $worksheet) {
1811 176
            $htmlPage .= "@page page$sheetId { ";
1812 176
            $left = StringHelper::formatNumber($worksheet->getPageMargins()->getLeft()) . 'in; ';
1813 176
            $htmlPage .= 'margin-left: ' . $left;
1814 176
            $right = StringHelper::FormatNumber($worksheet->getPageMargins()->getRight()) . 'in; ';
1815 176
            $htmlPage .= 'margin-right: ' . $right;
1816 176
            $top = StringHelper::FormatNumber($worksheet->getPageMargins()->getTop()) . 'in; ';
1817 176
            $htmlPage .= 'margin-top: ' . $top;
1818 176
            $bottom = StringHelper::FormatNumber($worksheet->getPageMargins()->getBottom()) . 'in; ';
1819 176
            $htmlPage .= 'margin-bottom: ' . $bottom;
1820 176
            $orientation = $worksheet->getPageSetup()->getOrientation();
1821 176
            if ($orientation === \PhpOffice\PhpSpreadsheet\Worksheet\PageSetup::ORIENTATION_LANDSCAPE) {
1822 5
                $htmlPage .= 'size: landscape; ';
1823 171
            } elseif ($orientation === \PhpOffice\PhpSpreadsheet\Worksheet\PageSetup::ORIENTATION_PORTRAIT) {
1824 4
                $htmlPage .= 'size: portrait; ';
1825
            }
1826 176
            $htmlPage .= "}\n";
1827 176
            ++$sheetId;
1828
        }
1829
        $htmlPage .= <<<EOF
1830 176
.navigation {page-break-after: always;}
1831
.scrpgbrk, div + div {page-break-before: always;}
1832
@media screen {
1833
  .gridlines td {border: 1px solid black;}
1834
  .gridlines th {border: 1px solid black;}
1835
  body>div {margin-top: 5px;}
1836
  body>div:first-child {margin-top: 0;}
1837
  .scrpgbrk {margin-top: 1px;}
1838
}
1839
@media print {
1840
  .gridlinesp td {border: 1px solid black;}
1841
  .gridlinesp th {border: 1px solid black;}
1842
  .navigation {display: none;}
1843
}
1844
1845
EOF;
1846 176
        $htmlPage .= $generateSurroundingHTML ? ('</style>' . PHP_EOL) : '';
1847
1848 176
        return $htmlPage;
1849
    }
1850
}
1851