Passed
Pull Request — master (#4204)
by
unknown
10:49
created

Html::generateRowWriteCell()   C

Complexity

Conditions 16
Paths 66

Size

Total Lines 80
Code Lines 39

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 37
CRAP Score 16

Importance

Changes 0
Metric Value
eloc 39
dl 0
loc 80
ccs 37
cts 37
cp 1
rs 5.5666
c 0
b 0
f 0
cc 16
nc 66
nop 11
crap 16

How to fix   Long Method    Complexity    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Writer;
4
5
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
6
use PhpOffice\PhpSpreadsheet\Cell\Cell;
7
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
8
use PhpOffice\PhpSpreadsheet\Chart\Chart;
9
use PhpOffice\PhpSpreadsheet\Comment;
10
use PhpOffice\PhpSpreadsheet\Document\Properties;
11
use PhpOffice\PhpSpreadsheet\RichText\RichText;
12
use PhpOffice\PhpSpreadsheet\RichText\Run;
13
use PhpOffice\PhpSpreadsheet\Settings;
14
use PhpOffice\PhpSpreadsheet\Shared\Date;
15
use PhpOffice\PhpSpreadsheet\Shared\Drawing as SharedDrawing;
16
use PhpOffice\PhpSpreadsheet\Shared\File;
17
use PhpOffice\PhpSpreadsheet\Shared\Font as SharedFont;
18
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
19
use PhpOffice\PhpSpreadsheet\Spreadsheet;
20
use PhpOffice\PhpSpreadsheet\Style\Alignment;
21
use PhpOffice\PhpSpreadsheet\Style\Border;
22
use PhpOffice\PhpSpreadsheet\Style\Borders;
23
use PhpOffice\PhpSpreadsheet\Style\Fill;
24
use PhpOffice\PhpSpreadsheet\Style\Font;
25
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
26
use PhpOffice\PhpSpreadsheet\Style\Style;
27
use PhpOffice\PhpSpreadsheet\Worksheet\BaseDrawing;
28
use PhpOffice\PhpSpreadsheet\Worksheet\Drawing;
29
use PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing;
30
use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup;
31
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
32
use PhpOffice\PhpSpreadsheet\Writer\BaseWriter;
33
34
class Html extends BaseWriter
35
{
36
  private const DEFAULT_CELL_WIDTH_POINTS = 42;
37
38
  private const DEFAULT_CELL_WIDTH_PIXELS = 56;
39
40
  /**
41
   * Migration aid to tell if html tags will be treated as plaintext in comments.
42
   *     if (
43
   *         defined(
44
   *             \PhpOffice\PhpSpreadsheet\Writer\Html::class
45
   *             . '::COMMENT_HTML_TAGS_PLAINTEXT'
46
   *         )
47
   *     ) {
48
   *         new logic with styling in TextRun elements
49
   *     } else {
50
   *         old logic with styling via Html tags
51
   *     }.
52
   */
53
  public const COMMENT_HTML_TAGS_PLAINTEXT = true;
54
55
  /**
56
   * Spreadsheet object.
57
   */
58
  protected Spreadsheet $spreadsheet;
59
60
  /**
61
   * Sheet index to write.
62
   */
63
  private null| int | array $sheetIndex = 0;
64
65
  /**
66
   * Images root.
67
   */
68
  private string $imagesRoot = '';
69
70
  /**
71
   * embed images, or link to images.
72
   */
73
  protected bool $embedImages = false;
74
75
  /**
76
   * Use inline CSS?
77
   */
78
  private bool $useInlineCss = false;
79
80
  /**
81
   * Array of CSS styles.
82
   */
83
  private ?array $cssStyles = null;
84
85
  /**
86
   * Array of column widths in points.
87
   */
88
  private array $columnWidths;
89
90
  /**
91
   * Default font.
92
   */
93
  private Font $defaultFont;
94
95
  /**
96
   * Flag whether spans have been calculated.
97
   */
98
  private bool $spansAreCalculated = false;
99
100
  /**
101
   * Excel cells that should not be written as HTML cells.
102
   */
103
  private array $isSpannedCell = [];
104
105
  /**
106
   * Excel cells that are upper-left corner in a cell merge.
107
   */
108
  private array $isBaseCell = [];
109
110
  /**
111
   * Excel rows that should not be written as HTML rows.
112
   */
113
  private array $isSpannedRow = [];
114
115
  /**
116
   * Is the current writer creating PDF?
117
   */
118
  protected bool $isPdf = false;
119
120
  /**
121
   * Is the current writer creating mPDF?
122
   *
123
   * @deprecated 2.0.1 use instanceof Mpdf instead
124
   */
125
  protected bool $isMPdf = false;
126
127
  /**
128
   * Generate the Navigation block.
129
   */
130
  private bool $generateSheetNavigationBlock = true;
131
132
  /**
133
   * Callback for editing generated html.
134
   *
135
   * @var null|callable
136
   */
137
  private $editHtmlCallback;
138
139
  /** @var BaseDrawing[] */
140
  private $sheetDrawings;
141
142
  /** @var Chart[] */
143
  private $sheetCharts;
144
145
  /**
146
   * Create a new HTML.
147 516
   */
148
  public function __construct(Spreadsheet $spreadsheet)
149 516
  {
150 516
    $this->spreadsheet = $spreadsheet;
151
    $this->defaultFont = $this->spreadsheet->getDefaultStyle()->getFont();
152
  }
153
154
  /**
155
   * Save Spreadsheet to file.
156
   *
157
   * @param resource|string $filename
158 445
   */
159
  public function save($filename, int $flags = 0): void
160 445
  {
161
    $this->processFlags($flags);
162
163 445
    // Open file
164
    $this->openFileHandle($filename);
165
166 444
    // Write html
167
    fwrite($this->fileHandle, $this->generateHTMLAll());
168
169 444
    // Close file
170
    $this->maybeCloseFileHandle();
171
  }
172
173
  /**
174
   * Save Spreadsheet as html to variable.
175 505
   */
176
  public function generateHtmlAll(): string
177 505
  {
178 505
    $sheets = $this->generateSheetPrep();
179 505
    foreach ($sheets as $sheet) {
180
      $sheet->calculateArrays($this->preCalculateFormulas);
181
    }
182 505
    // garbage collect
183
    $this->spreadsheet->garbageCollect();
184 505
185 505
    $saveDebugLog = Calculation::getInstance($this->spreadsheet)->getDebugLog()->getWriteDebugLog();
186
    Calculation::getInstance($this->spreadsheet)->getDebugLog()->setWriteDebugLog(false);
187
188 505
    // Build CSS
189
    $this->buildCSS(!$this->useInlineCss);
190 505
191
    $html = '';
192
193 505
    // Write headers
194
    $html .= $this->generateHTMLHeader(!$this->useInlineCss);
195
196 505
    // Write navigation (tabs)
197 478
    if ((!$this->isPdf) && ($this->generateSheetNavigationBlock)) {
198
      $html .= $this->generateNavigation();
199
    }
200
201 505
    // Write data
202
    $html .= $this->generateSheetData();
203
204 505
    // Write footer
205 505
    $html .= $this->generateHTMLFooter();
206 505
    $callback = $this->editHtmlCallback;
207 6
    if ($callback) {
208
      $html = $callback($html);
209
    }
210 505
211
    Calculation::getInstance($this->spreadsheet)->getDebugLog()->setWriteDebugLog($saveDebugLog);
212 505
213
    return $html;
214
  }
215
216
  /**
217
   * Set a callback to edit the entire HTML.
218
   *
219
   * The callback must accept the HTML as string as first parameter,
220
   * and it must return the edited HTML as string.
221 6
   */
222
  public function setEditHtmlCallback(?callable $callback): void
223 6
  {
224
    $this->editHtmlCallback = $callback;
225
  }
226
227
  /**
228
   * Map VAlign.
229
   *
230
   * @param string $vAlign Vertical alignment
231 507
   */
232
  private function mapVAlign(string $vAlign): string
233 507
  {
234
    return Alignment::VERTICAL_ALIGNMENT_FOR_HTML[$vAlign] ?? '';
235
  }
236
237
  /**
238
   * Map HAlign.
239
   *
240
   * @param string $hAlign Horizontal alignment
241 507
   */
242
  private function mapHAlign(string $hAlign): string
243 507
  {
244
    return Alignment::HORIZONTAL_ALIGNMENT_FOR_HTML[$hAlign] ?? '';
245
  }
246
247
  const BORDER_NONE = 'none';
248
  const BORDER_ARR = [
249
    Border::BORDER_NONE => self::BORDER_NONE,
250
    Border::BORDER_DASHDOT => '1px dashed',
251
    Border::BORDER_DASHDOTDOT => '1px dotted',
252
    Border::BORDER_DASHED => '1px dashed',
253
    Border::BORDER_DOTTED => '1px dotted',
254
    Border::BORDER_DOUBLE => '3px double',
255
    Border::BORDER_HAIR => '1px solid',
256
    Border::BORDER_MEDIUM => '2px solid',
257
    Border::BORDER_MEDIUMDASHDOT => '2px dashed',
258
    Border::BORDER_MEDIUMDASHDOTDOT => '2px dotted',
259
    Border::BORDER_SLANTDASHDOT => '2px dashed',
260
    Border::BORDER_THICK => '3px solid',
261
  ];
262
263
  /**
264
   * Map border style.
265
   *
266
   * @param int|string $borderStyle Sheet index
267 496
   */
268
  private function mapBorderStyle($borderStyle): string
269 496
  {
270
    return self::BORDER_ARR[$borderStyle] ?? '1px solid';
271
  }
272
273
  /**
274
   * Get sheet index.
275 17
   */
276
  public function getSheetIndex(): null | int | array
277 17
  {
278
    return $this->sheetIndex;
279
  }
280
281
  /**
282
   * Set sheet index.
283
   *
284
   * @param int $sheetIndex Sheet index
285
   *
286
   * @return $this
287 1
   */
288
  public function setSheetIndex(int | array $sheetIndex): static
289 1
  {
290
    $this->sheetIndex = $sheetIndex;
291 1
292
    return $this;
293
  }
294
295
  /**
296
   * Get sheet index.
297 1
   */
298
  public function getGenerateSheetNavigationBlock(): bool
299 1
  {
300
    return $this->generateSheetNavigationBlock;
301
  }
302
303
  /**
304
   * Set sheet index.
305
   *
306
   * @param bool $generateSheetNavigationBlock Flag indicating whether the sheet navigation block should be generated or not
307
   *
308
   * @return $this
309 1
   */
310
  public function setGenerateSheetNavigationBlock(bool $generateSheetNavigationBlock): static
311 1
  {
312
    $this->generateSheetNavigationBlock = (bool) $generateSheetNavigationBlock;
313 1
314
    return $this;
315
  }
316
317
  /**
318
   * Write all sheets (resets sheetIndex to NULL).
319
   *
320
   * @return $this
321 14
   */
322
  public function writeAllSheets(): static
323 14
  {
324
    $this->sheetIndex = null;
325 14
326
    return $this;
327
  }
328 507
329
  private static function generateMeta(?string $val, string $desc): string
330 507
  {
331 507
    return ($val || $val === '0')
332 507
      ? ('      <meta name="' . $desc . '" content="' . htmlspecialchars($val, Settings::htmlEntityFlags()) . '" />' . PHP_EOL)
333
      : '';
334
  }
335
336
  public const BODY_LINE = '  <body>' . PHP_EOL;
337
338
  private const CUSTOM_TO_META = [
339
    Properties::PROPERTY_TYPE_BOOLEAN => 'bool',
340
    Properties::PROPERTY_TYPE_DATE => 'date',
341
    Properties::PROPERTY_TYPE_FLOAT => 'float',
342
    Properties::PROPERTY_TYPE_INTEGER => 'int',
343
    Properties::PROPERTY_TYPE_STRING => 'string',
344
  ];
345
346
  /**
347
   * Generate HTML header.
348
   *
349
   * @param bool $includeStyles Include styles?
350 507
   */
351
  public function generateHTMLHeader(bool $includeStyles = false): string
352
  {
353 507
    // Construct HTML
354 507
    $properties = $this->spreadsheet->getProperties();
355 507
    $html = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">' . PHP_EOL;
356 507
    $html .= '<html xmlns="http://www.w3.org/1999/xhtml">' . PHP_EOL;
357 507
    $html .= '  <head>' . PHP_EOL;
358 507
    $html .= '      <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />' . PHP_EOL;
359 507
    $html .= '      <meta name="generator" content="PhpSpreadsheet, https://github.com/PHPOffice/PhpSpreadsheet" />' . PHP_EOL;
360 507
    $title = $properties->getTitle();
361 7
    if ($title === '') {
362
      $title = $this->spreadsheet->getActiveSheet()->getTitle();
363 507
    }
364 507
    $html .= '      <title>' . htmlspecialchars($title, Settings::htmlEntityFlags()) . '</title>' . PHP_EOL;
365 507
    $html .= self::generateMeta($properties->getCreator(), 'author');
366 507
    $html .= self::generateMeta($properties->getTitle(), 'title');
367 507
    $html .= self::generateMeta($properties->getDescription(), 'description');
368 507
    $html .= self::generateMeta($properties->getSubject(), 'subject');
369 507
    $html .= self::generateMeta($properties->getKeywords(), 'keywords');
370 507
    $html .= self::generateMeta($properties->getCategory(), 'category');
371 507
    $html .= self::generateMeta($properties->getCompany(), 'company');
372 507
    $html .= self::generateMeta($properties->getManager(), 'manager');
373 507
    $html .= self::generateMeta($properties->getLastModifiedBy(), 'lastModifiedBy');
374 507
    $html .= self::generateMeta($properties->getViewport(), 'viewport');
375 507
    $date = Date::dateTimeFromTimestamp((string) $properties->getCreated());
376 507
    $date->setTimeZone(Date::getDefaultOrLocalTimeZone());
377 507
    $html .= self::generateMeta($date->format(DATE_W3C), 'created');
378 507
    $date = Date::dateTimeFromTimestamp((string) $properties->getModified());
379 507
    $date->setTimeZone(Date::getDefaultOrLocalTimeZone());
380
    $html .= self::generateMeta($date->format(DATE_W3C), 'modified');
381 507
382 507
    $customProperties = $properties->getCustomProperties();
383 2
    foreach ($customProperties as $customProperty) {
384 2
      $propertyValue = $properties->getCustomPropertyValue($customProperty);
385 2
      $propertyType = $properties->getCustomPropertyType($customProperty);
386 2
      $propertyQualifier = self::CUSTOM_TO_META[$propertyType] ?? null;
387 2
      if ($propertyQualifier !== null) {
388 1
        if ($propertyType === Properties::PROPERTY_TYPE_BOOLEAN) {
389 2
          $propertyValue = $propertyValue ? '1' : '0';
390 1
        } elseif ($propertyType === Properties::PROPERTY_TYPE_DATE) {
391 1
          $date = Date::dateTimeFromTimestamp((string) $propertyValue);
392 1
          $date->setTimeZone(Date::getDefaultOrLocalTimeZone());
393
          $propertyValue = $date->format(DATE_W3C);
394 2
        } else {
395
          $propertyValue = (string) $propertyValue;
396 2
        }
397
        $html .= self::generateMeta($propertyValue, "custom.$propertyQualifier.$customProperty");
398
      }
399
    }
400 507
401 1
    if (!empty($properties->getHyperlinkBase())) {
402
      $html .= '      <base href="' . $properties->getHyperlinkBase() . '" />' . PHP_EOL;
403
    }
404 507
405
    $html .= $includeStyles ? $this->generateStyles(true) : $this->generatePageDeclarations(true);
406 507
407 507
    $html .= '  </head>' . PHP_EOL;
408 507
    $html .= '' . PHP_EOL;
409
    $html .= self::BODY_LINE;
410 507
411
    return $html;
412
  }
413
414 505
  /** @return Worksheet[] */
415
  private function generateSheetPrep(): array
416
  {
417 505
    // Fetch sheets
418 13
    if (is_array($this->sheetIndex)) {
419
      $sheets = [];
420 499
      foreach ($this->sheetIndex as $sheetIndex) {
421
        $sheets[] = $this->spreadsheet->getSheet($sheetIndex);
422
      }
423 505
    } elseif ($this->sheetIndex === null) {
424
      $sheets = $this->spreadsheet->getAllSheets();
425
    } else {
426 505
      $sheets = [$this->spreadsheet->getSheet($this->sheetIndex)];
427
    }
428
429 505
    return $sheets;
430 505
  }
431 505
432 2
  private function generateSheetStarts(Worksheet $sheet, int $rowMin): array
433
  {
434
    // calculate start of <tbody>, <thead>
435 2
    $tbodyStart = $rowMin;
436 2
    $theadStart = $theadEnd = 0; // default: no <thead>    no </thead>
437 2
    if ($sheet->getPageSetup()->isRowsToRepeatAtTopSet()) {
438 2
      $rowsToRepeatAtTop = $sheet->getPageSetup()->getRowsToRepeatAtTop();
439
440
      // we can only support repeating rows that start at top row
441
      if ($rowsToRepeatAtTop[0] == 1) {
442 505
        $theadStart = $rowsToRepeatAtTop[0];
443
        $theadEnd = $rowsToRepeatAtTop[1];
444
        $tbodyStart = $rowsToRepeatAtTop[1] + 1;
445 505
      }
446
    }
447
448 505
    return [$theadStart, $theadEnd, $tbodyStart];
449 505
  }
450 505
451
  private function generateSheetTags(int $row, int $theadStart, int $theadEnd, int $tbodyStart): array
452 505
  {
453 505
    // <thead> ?
454
    $startTag = ($row == $theadStart) ? ('        <thead>' . PHP_EOL) : '';
455 505
    if (!$startTag) {
456
      $startTag = ($row == $tbodyStart) ? ('        <tbody>' . PHP_EOL) : '';
457
    }
458
    $endTag = ($row == $theadEnd) ? ('        </thead>' . PHP_EOL) : '';
459
    $cellType = ($row >= $tbodyStart) ? 'td' : 'th';
460
461 505
    return [$cellType, $startTag, $endTag];
462
  }
463
464 505
  /**
465 505
   * Generate sheet data.
466
   */
467
  public function generateSheetData(): string
468 505
  {
469
    // Ensure that Spans have been calculated?
470
    $this->calculateSpans();
471 505
    $sheets = $this->generateSheetPrep();
472 505
473
    // Construct HTML
474 505
    $html = '';
475 505
476 505
    // Loop all sheets
477
    $sheetId = 0;
478
    foreach ($sheets as $sheet) {
479 505
      // Write table header
480 505
      $html .= $this->generateTableHeader($sheet);
481 505
      $this->sheetCharts = [];
482 505
      $this->sheetDrawings = [];
483
484 505
      // Get worksheet dimension
485
      [$min, $max] = explode(':', $sheet->calculateWorksheetDataDimension());
486
      [$minCol, $minRow, $minColString] = Coordinate::indexesFromString($min);
487 505
      [$maxCol, $maxRow] = Coordinate::indexesFromString($max);
488 505
      $this->extendRowsAndColumns($sheet, $maxCol, $maxRow);
489 505
490 505
      [$theadStart, $theadEnd, $tbodyStart] = $this->generateSheetStarts($sheet, $minRow);
491
492
      // Loop through cells
493 505
      $row = $minRow - 1;
494
      while ($row++ < $maxRow) {
495 505
        [$cellType, $startTag, $endTag] = $this->generateSheetTags($row, $theadStart, $theadEnd, $tbodyStart);
496
        $html .= $startTag;
497 505
498 505
        // Write row if there are HTML table cells in it
499 505
        if ($this->shouldGenerateRow($sheet, $row) && !isset($this->isSpannedRow[$sheet->getParentOrThrow()->getIndex($sheet)][$row])) {
500
          // Start a new rowData
501 505
          $rowData = [];
502 505
          // Loop through columns
503 505
          $column = $minCol;
504
          $colStr = $minColString;
505 505
          while ($column <= $maxCol) {
506 505
            // Cell exists?
507
            $cellAddress = Coordinate::stringFromColumnIndex($column) . $row;
508 505
            if ($this->shouldGenerateColumn($sheet, $colStr)) {
509
              $rowData[$column] = ($sheet->getCellCollection()->has($cellAddress)) ? $cellAddress : '';
510
            }
511 505
            ++$column;
512
            ++$colStr;
513
          }
514
          $html .= $this->generateRow($sheet, $rowData, $row - 1, $cellType);
515 505
        }
516
517 505
        $html .= $endTag;
518 6
      }
519 1
520
      // Write table footer
521
      $html .= $this->generateTableFooter();
522
      // Writing PDF?
523
      if ($this->isPdf && $this->useInlineCss) {
524 505
        if ($this->sheetIndex === null && $sheetId + 1 < $this->spreadsheet->getSheetCount()) {
525
          $html .= '<div style="page-break-before:always" ></div>';
526
        }
527 505
      }
528
529
      // Next sheet
530
      ++$sheetId;
531
    }
532
533 478
    return $html;
534
  }
535
536 478
  /**
537 478
   * Generate sheet tabs.
538 7
   */
539
  public function generateNavigation(): string
540 477
  {
541
    // Fetch sheets
542
    $sheets = [];
543
    if ($this->sheetIndex === null) {
544 478
      $sheets = $this->spreadsheet->getAllSheets();
545
    } else {
546
      $sheets[] = $this->spreadsheet->getSheet($this->sheetIndex);
547 478
    }
548
549 7
    // Construct HTML
550
    $html = '';
551 7
552
    // Only if there are more than 1 sheets
553 7
    if (count($sheets) > 1) {
554 7
      // Loop all sheets
555 7
      $sheetId = 0;
556
557
      $html .= '<ul class="navigation">' . PHP_EOL;
558 7
559
      foreach ($sheets as $sheet) {
560
        $html .= '  <li class="sheet' . $sheetId . '"><a href="#sheet' . $sheetId . '">' . $sheet->getTitle() . '</a></li>' . PHP_EOL;
561 478
        ++$sheetId;
562
      }
563
564 505
      $html .= '</ul>' . PHP_EOL;
565
    }
566 505
567 4
    return $html;
568 4
  }
569 4
570 4
  private function extendRowsAndColumns(Worksheet $worksheet, int &$colMax, int &$rowMax): void
571 4
  {
572 4
    if ($this->includeCharts) {
573 2
      foreach ($worksheet->getChartCollection() as $chart) {
574
        if ($chart instanceof Chart) {
575 4
          $chartCoordinates = $chart->getTopLeftPosition();
576 2
          $this->sheetCharts[$chartCoordinates['cell']] = $chart;
577
          $chartTL = Coordinate::indexesFromString($chartCoordinates['cell']);
578
          if ($chartTL[1] > $rowMax) {
579
            $rowMax = $chartTL[1];
580
          }
581 505
          if ($chartTL[0] > $colMax) {
582 24
            $colMax = $chartTL[0];
583
          }
584
        }
585 24
      }
586 24
    }
587 24
    foreach ($worksheet->getDrawingCollection() as $drawing) {
588 3
      if ($drawing instanceof Drawing && $drawing->getPath() === '') {
589
        continue;
590 24
      }
591 1
      $imageTL = Coordinate::indexesFromString($drawing->getCoordinates());
592
      $this->sheetDrawings[$drawing->getCoordinates()] = $drawing;
593
      if ($imageTL[1] > $rowMax) {
594
        $rowMax = $imageTL[1];
595
      }
596
      if ($imageTL[0] > $colMax) {
597
        $colMax = $imageTL[0];
598
      }
599
    }
600
  }
601 18
602
  /**
603
   * Convert Windows file name to file protocol URL.
604 18
   *
605 1
   * @param string $filename file name on local system
606 1
   */
607
  public static function winFileToUrl(string $filename, bool $mpdf = false): string
608
  {
609 18
    // Windows filename
610
    if (substr($filename, 1, 2) === ':\\') {
611
      $protocol = $mpdf ? '' : 'file:///';
612
      $filename = $protocol . str_replace('\\', '/', $filename);
613
    }
614
615
    return $filename;
616
  }
617 505
618
  /**
619
   * Generate image tag in cell.
620 505
   *
621
   * @param string $coordinates Cell coordinates
622
   */
623 505
  private function writeImageInCell(string $coordinates): string
624 505
  {
625 24
    // Construct HTML
626 24
    $html = '';
627 24
628 3
    // Write images
629 3
    $drawing = $this->sheetDrawings[$coordinates] ?? null;
630 3
    if ($drawing !== null) {
631
      $filedesc = $drawing->getDescription();
632
      $filedesc = $filedesc ? htmlspecialchars($filedesc, ENT_QUOTES) : 'Embedded image';
633 24
      if ($drawing instanceof Drawing && $drawing->getPath() !== '') {
634 24
        $filename = $drawing->getPath();
635 24
636 17
        // Strip off eventual '.'
637
        $filename = (string) preg_replace('/^[.]/', '', $filename);
638
639 17
        // Prepend images root
640
        $filename = $this->getImagesRoot() . $filename;
641
642 17
        // Strip off eventual '.' if followed by non-/
643
        $filename = (string) preg_replace('@^[.]([^/])@', '$1', $filename);
644
645 17
        // Convert UTF8 data to PCDATA
646
        $filename = htmlspecialchars($filename, Settings::htmlEntityFlags());
647
648 17
        $html .= PHP_EOL;
649
        $imageData = self::winFileToUrl($filename, $this instanceof Pdf\Mpdf);
650 17
651 17
        if ($this->embedImages || str_starts_with($imageData, 'zip://')) {
652
          $imageData = 'data:,';
653 17
          $picture = @file_get_contents($filename);
654 10
          if ($picture !== false) {
655 10
            $mimeContentType = (string) @mime_content_type($filename);
656 10
            if (str_starts_with($mimeContentType, 'image/')) {
657 10
              // base64 encode the binary data
658 10
              $base64 = base64_encode($picture);
659
              $imageData = 'data:' . $mimeContentType . ';base64,' . $base64;
660 10
            }
661 10
          }
662
        }
663
664
        $html .= '<img style="position: absolute; z-index: 1; left: '
665
          . $drawing->getOffsetX() . 'px; top: ' . $drawing->getOffsetY() . 'px; width: '
666 17
          . $drawing->getWidth() . 'px; height: ' . $drawing->getHeight() . 'px;" src="'
667 17
          . $imageData . '" alt="' . $filedesc . '" />';
668 17
      } elseif ($drawing instanceof MemoryDrawing) {
669 17
        $imageResource = $drawing->getImageResource();
670 7
        if ($imageResource) {
671 7
          ob_start(); //  Let's start output buffering.
672 7
          imagepng($imageResource); //  This will normally output the image, but because of ob_start(), it won't.
673 7
          $contents = (string) ob_get_contents(); //  Instead, output above is saved to $contents
674 7
          ob_end_clean(); //  End the output buffer.
675 7
676 7
          $dataUri = 'data:image/png;base64,' . base64_encode($contents);
677
678 7
          //  Because of the nature of tables, width is more important than height.
679
          //  max-width: 100% ensures that image doesnt overflow containing cell
680
          //    However, PR #3535 broke test
681
          //    25_In_memory_image, apparently because
682
          //    of the use of max-with. In addition,
683
          //    non-memory-drawings don't use max-width.
684
          //    Its use here is suspect and is being eliminated.
685
          //  width: X sets width of supplied image.
686
          //  As a result, images bigger than cell will be contained and images smaller will not get stretched
687
          $html .= '<img alt="' . $filedesc . '" src="' . $dataUri . '" style="width:' . $drawing->getWidth() . 'px;left: '
688
            . $drawing->getOffsetX() . 'px; top: ' . $drawing->getOffsetY() . 'px;position: absolute; z-index: 1;" />';
689 7
        }
690 7
      }
691
    }
692
693
    return $html;
694
  }
695 505
696
  /**
697
   * Generate chart tag in cell.
698
   * This code should be exercised by sample:
699
   * Chart/32_Chart_read_write_PDF.php.
700
   */
701
  private function writeChartInCell(Worksheet $worksheet, string $coordinates): string
702
  {
703 4
    // Construct HTML
704
    $html = '';
705
706 4
    // Write charts
707
    $chart = $this->sheetCharts[$coordinates] ?? null;
708
    if ($chart !== null) {
709 4
      $chartCoordinates = $chart->getTopLeftPosition();
710 4
      $chartFileName = File::sysGetTempDir() . '/' . uniqid('', true) . '.png';
711 4
      $renderedWidth = $chart->getRenderedWidth();
712 4
      $renderedHeight = $chart->getRenderedHeight();
713 4
      if ($renderedWidth === null || $renderedHeight === null) {
714 4
        $this->adjustRendererPositions($chart, $worksheet);
715 4
      }
716 4
      $title = $chart->getTitle();
717
      $caption = null;
718 4
      $filedesc = '';
719 4
      if ($title !== null) {
720 4
        $calculatedTitle = $title->getCalculatedTitle($worksheet->getParent());
721 4
        if ($calculatedTitle !== null) {
722 4
          $caption = $title->getCaption();
723 4
          $title->setCaption($calculatedTitle);
724 2
        }
725 2
        $filedesc = $title->getCaptionText($worksheet->getParent());
726
      }
727 4
      $renderSuccessful = $chart->render($chartFileName);
728
      $chart->setRenderedWidth($renderedWidth);
729 4
      $chart->setRenderedHeight($renderedHeight);
730 4
      if (isset($title, $caption)) {
731 4
        $title->setCaption($caption);
732 4
      }
733 2
      if (!$renderSuccessful) {
734
        return '';
735 4
      }
736
737
      $html .= PHP_EOL;
738
      $imageDetails = getimagesize($chartFileName) ?: ['', '', 'mime' => ''];
739 4
740 4
      $filedesc = $filedesc ? htmlspecialchars($filedesc, ENT_QUOTES) : 'Embedded chart';
741
      $picture = file_get_contents($chartFileName);
742 4
      unlink($chartFileName);
743 4
      if ($picture !== false) {
744 4
        $base64 = base64_encode($picture);
745 4
        $imageData = 'data:' . $imageDetails['mime'] . ';base64,' . $base64;
746 4
747 4
        $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;
748
      }
749 4
    }
750
751
    // Return
752
    return $html;
753
  }
754 4
755
  private function adjustRendererPositions(Chart $chart, Worksheet $sheet): void
756
  {
757 4
    $topLeft = $chart->getTopLeftPosition();
758
    $bottomRight = $chart->getBottomRightPosition();
759 4
    $tlCell = $topLeft['cell'];
760 4
    $brCell = $bottomRight['cell'];
761 4
    if ($tlCell !== '' && $brCell !== '') {
762 4
      $tlCoordinate = Coordinate::indexesFromString($tlCell);
763 4
      $brCoordinate = Coordinate::indexesFromString($brCell);
764 4
      $totalHeight = 0.0;
765 4
      $totalWidth = 0.0;
766 4
      $defaultRowHeight = $sheet->getDefaultRowDimension()->getRowHeight();
767 4
      $defaultRowHeight = SharedDrawing::pointsToPixels(($defaultRowHeight >= 0) ? $defaultRowHeight : SharedFont::getDefaultRowHeightByFont($this->defaultFont));
768 4
      if ($tlCoordinate[1] <= $brCoordinate[1] && $tlCoordinate[0] <= $brCoordinate[0]) {
769 4
        for ($row = $tlCoordinate[1]; $row <= $brCoordinate[1]; ++$row) {
770 4
          $height = $sheet->getRowDimension($row)->getRowHeight('pt');
771 4
          $totalHeight += ($height >= 0) ? $height : $defaultRowHeight;
772 4
        }
773 4
        $rightEdge = $brCoordinate[2];
774
        ++$rightEdge;
775 4
        for ($column = $tlCoordinate[2]; $column !== $rightEdge; ++$column) {
776 4
          $width = $sheet->getColumnDimension($column)->getWidth();
777 4
          $width = ($width < 0) ? self::DEFAULT_CELL_WIDTH_PIXELS : SharedDrawing::cellDimensionToPixels($sheet->getColumnDimension($column)->getWidth(), $this->defaultFont);
778 4
          $totalWidth += $width;
779 4
        }
780 4
        $chart->setRenderedWidth($totalWidth);
781
        $chart->setRenderedHeight($totalHeight);
782 4
      }
783 4
    }
784
  }
785
786
  /**
787
   * Generate CSS styles.
788
   *
789
   * @param bool $generateSurroundingHTML Generate surrounding HTML tags? (&lt;style&gt; and &lt;/style&gt;)
790
   */
791
  public function generateStyles(bool $generateSurroundingHTML = true): string
792
  {
793 500
    // Build CSS
794
    $css = $this->buildCSS($generateSurroundingHTML);
795
796 500
    // Construct HTML
797
    $html = '';
798
799 500
    // Start styles
800
    if ($generateSurroundingHTML) {
801
      $html .= '    <style type="text/css">' . PHP_EOL;
802 500
      $html .= (array_key_exists('html', $css)) ? ('      html { ' . $this->assembleCSS($css['html']) . ' }' . PHP_EOL) : '';
803 500
    }
804 500
805
    // Write all other styles
806
    foreach ($css as $styleName => $styleDefinition) {
807
      if ($styleName != 'html') {
808 500
        $html .= '      ' . $styleName . ' { ' . $this->assembleCSS($styleDefinition) . ' }' . PHP_EOL;
809 500
      }
810 500
    }
811
    $html .= $this->generatePageDeclarations(false);
812
813 500
    // End styles
814
    if ($generateSurroundingHTML) {
815
      $html .= '    </style>' . PHP_EOL;
816 500
    }
817 500
818
    // Return
819
    return $html;
820
  }
821 500
822
  private function buildCssRowHeights(Worksheet $sheet, array &$css, int $sheetIndex): void
823
  {
824 507
    // Calculate row heights
825
    foreach ($sheet->getRowDimensions() as $rowDimension) {
826
      $row = $rowDimension->getRowIndex() - 1;
827 507
828 19
      // table.sheetN tr.rowYYYYYY { }
829
      $css['table.sheet' . $sheetIndex . ' tr.row' . $row] = [];
830
831 19
      if ($rowDimension->getRowHeight() != -1) {
832
        $pt_height = $rowDimension->getRowHeight();
833 19
        $css['table.sheet' . $sheetIndex . ' tr.row' . $row]['height'] = $pt_height . 'pt';
834 9
      }
835 9
      if ($rowDimension->getVisible() === false) {
836
        $css['table.sheet' . $sheetIndex . ' tr.row' . $row]['display'] = 'none';
837 19
        $css['table.sheet' . $sheetIndex . ' tr.row' . $row]['visibility'] = 'hidden';
838 8
      }
839 8
    }
840
  }
841
842
  private function buildCssPerSheet(Worksheet $sheet, array &$css): void
843
  {
844 507
    // Calculate hash code
845
    $sheetIndex = $sheet->getParentOrThrow()->getIndex($sheet);
846
    $setup = $sheet->getPageSetup();
847 507
    if ($setup->getFitToPage() && $setup->getFitToHeight() === 1) {
848 507
      $css["table.sheet$sheetIndex"]['page-break-inside'] = 'avoid';
849 507
      $css["table.sheet$sheetIndex"]['break-inside'] = 'avoid';
850 2
    }
851 2
    $picture = $sheet->getBackgroundImage();
852
    if ($picture !== '') {
853 507
      $base64 = base64_encode($picture);
854 507
      $css["table.sheet$sheetIndex"]['background-image'] = 'url(data:' . $sheet->getBackgroundMime() . ';base64,' . $base64 . ')';
855 1
    }
856 1
857
    // Build styles
858
    // Calculate column widths
859
    $sheet->calculateColumnWidths();
860
861 507
    // col elements, initialize
862
    $highestColumnIndex = Coordinate::columnIndexFromString($sheet->getHighestColumn()) - 1;
863
    $column = -1;
864 507
    $colStr = 'A';
865 507
    while ($column++ < $highestColumnIndex) {
866 507
      $this->columnWidths[$sheetIndex][$column] = self::DEFAULT_CELL_WIDTH_POINTS; // approximation
867 507
      if ($this->shouldGenerateColumn($sheet, $colStr)) {
868 507
        $css['table.sheet' . $sheetIndex . ' col.col' . $column]['width'] = self::DEFAULT_CELL_WIDTH_POINTS . 'pt';
869 507
      }
870 507
      ++$colStr;
871
    }
872 507
873
    // col elements, loop through columnDimensions and set width
874
    foreach ($sheet->getColumnDimensions() as $columnDimension) {
875
      $column = Coordinate::columnIndexFromString($columnDimension->getColumnIndex()) - 1;
876 507
      $width = SharedDrawing::cellDimensionToPixels($columnDimension->getWidth(), $this->defaultFont);
877 25
      $width = SharedDrawing::pixelsToPoints($width);
878 25
      if ($columnDimension->getVisible() === false) {
879 25
        $css['table.sheet' . $sheetIndex . ' .column' . $column]['display'] = 'none';
880 25
        // This would be better but Firefox has an 11-year-old bug.
881 6
        // https://bugzilla.mozilla.org/show_bug.cgi?id=819045
882
        //$css['table.sheet' . $sheetIndex . ' col.col' . $column]['visibility'] = 'collapse';
883
      }
884
      if ($width >= 0) {
885
        $this->columnWidths[$sheetIndex][$column] = $width;
886 25
        $css['table.sheet' . $sheetIndex . ' col.col' . $column]['width'] = $width . 'pt';
887 18
      }
888 18
    }
889
890
    // Default row height
891
    $rowDimension = $sheet->getDefaultRowDimension();
892
893 507
    // table.sheetN tr { }
894
    $css['table.sheet' . $sheetIndex . ' tr'] = [];
895
896 507
    if ($rowDimension->getRowHeight() == -1) {
897
      $pt_height = SharedFont::getDefaultRowHeightByFont($this->spreadsheet->getDefaultStyle()->getFont());
898 507
    } else {
899 500
      $pt_height = $rowDimension->getRowHeight();
900
    }
901 7
    $css['table.sheet' . $sheetIndex . ' tr']['height'] = $pt_height . 'pt';
902
    if ($rowDimension->getVisible() === false) {
903 507
      $css['table.sheet' . $sheetIndex . ' tr']['display'] = 'none';
904 507
      $css['table.sheet' . $sheetIndex . ' tr']['visibility'] = 'hidden';
905 1
    }
906 1
907
    $this->buildCssRowHeights($sheet, $css, $sheetIndex);
908
  }
909 507
910
  /**
911
   * Build CSS styles.
912
   *
913
   * @param bool $generateSurroundingHTML Generate surrounding HTML style? (html { })
914
   */
915
  public function buildCSS(bool $generateSurroundingHTML = true): array
916
  {
917 507
    // Cached?
918
    if ($this->cssStyles !== null) {
919
      return $this->cssStyles;
920 507
    }
921 498
922
    // Ensure that spans have been calculated
923
    $this->calculateSpans();
924
925 507
    // Construct CSS
926
    $css = [];
927
928 507
    // Start styles
929
    if ($generateSurroundingHTML) {
930
      // html { }
931 507
      $css['html']['font-family'] = 'Calibri, Arial, Helvetica, sans-serif';
932
      $css['html']['font-size'] = '11pt';
933 499
      $css['html']['background-color'] = 'white';
934 499
    }
935 499
936
    // CSS for comments as found in LibreOffice
937
    $css['a.comment-indicator:hover + div.comment'] = [
938
      'background' => '#ffd',
939 507
      'position' => 'absolute',
940 507
      'display' => 'block',
941 507
      'border' => '1px solid black',
942 507
      'padding' => '0.5em',
943 507
    ];
944 507
945 507
    $css['a.comment-indicator'] = [
946
      'background' => 'red',
947 507
      'display' => 'inline-block',
948 507
      'border' => '1px solid black',
949 507
      'width' => '0.5em',
950 507
      'height' => '0.5em',
951 507
    ];
952 507
953 507
    $css['div.comment']['display'] = 'none';
954
955 507
    // table { }
956
    $css['table']['border-collapse'] = 'collapse';
957
958 507
    // .b {}
959
    $css['.b']['text-align'] = 'center'; // BOOL
960
961 507
    // .e {}
962
    $css['.e']['text-align'] = 'center'; // ERROR
963
964 507
    // .f {}
965
    $css['.f']['text-align'] = 'right'; // FORMULA
966
967 507
    // .inlineStr {}
968
    $css['.inlineStr']['text-align'] = 'left'; // INLINE
969
970 507
    // .n {}
971
    $css['.n']['text-align'] = 'right'; // NUMERIC
972
973 507
    // .s {}
974
    $css['.s']['text-align'] = 'left'; // STRING
975
976 507
    // Calculate cell style hashes
977
    foreach ($this->spreadsheet->getCellXfCollection() as $index => $style) {
978
      $css['td.style' . $index . ', th.style' . $index] = $this->createCSSStyle($style);
979 507
      //$css['th.style' . $index] = $this->createCSSStyle($style);
980 507
    }
981
982
    // Fetch sheets
983
    $sheets = [];
984
    if (is_array($this->sheetIndex)) {
985 507
      foreach ($this->sheetIndex as $sheetIndex) {
986 507
        $sheets[] = $this->spreadsheet->getSheet($sheetIndex);
987 14
      }
988
    } elseif ($this->sheetIndex === null) {
989 500
      $sheets = $this->spreadsheet->getAllSheets();
990
    } else {
991
      $sheets[] = $this->spreadsheet->getSheet($this->sheetIndex);
992
    }
993 507
994 507
    // Build styles per sheet
995
    foreach ($sheets as $sheet) {
996
      $this->buildCssPerSheet($sheet, $css);
997
    }
998 507
999 507
    // Cache
1000
    if ($this->cssStyles === null) {
1001
      $this->cssStyles = $css;
1002
    }
1003 507
1004
    // Return
1005
    return $css;
1006
  }
1007
1008
  /**
1009 507
   * Create CSS style.
1010
   */
1011
  private function createCSSStyle(Style $style): array
1012 507
  {
1013 507
    // Create CSS
1014 507
    return array_merge(
1015 507
      $this->createCSSStyleAlignment($style->getAlignment()),
1016 507
      $this->createCSSStyleBorders($style->getBorders()),
1017 507
      $this->createCSSStyleFont($style->getFont()),
1018
      $this->createCSSStyleFill($style->getFill())
1019
    );
1020
  }
1021
1022
  /**
1023 507
   * Create CSS style.
1024
   */
1025
  private function createCSSStyleAlignment(Alignment $alignment): array
1026 507
  {
1027
    // Construct CSS
1028
    $css = [];
1029 507
1030 507
    // Create CSS
1031 507
    $verticalAlign = $this->mapVAlign($alignment->getVertical() ?? '');
1032
    if ($verticalAlign) {
1033 507
      $css['vertical-align'] = $verticalAlign;
1034 507
    }
1035 13
    $textAlign = $this->mapHAlign($alignment->getHorizontal() ?? '');
1036 13
    if ($textAlign) {
1037 10
      $css['text-align'] = $textAlign;
1038
      if (in_array($textAlign, ['left', 'right'])) {
1039
        $css['padding-' . $textAlign] = (string) ((int) $alignment->getIndent() * 9) . 'px';
1040 507
      }
1041 507
    }
1042 5
    $rotation = $alignment->getTextRotation();
1043 1
    if ($rotation !== 0 && $rotation !== Alignment::TEXTROTATION_STACK_PHPSPREADSHEET) {
1044
      if ($this instanceof Pdf\Mpdf) {
1045 4
        $css['text-rotate'] = "$rotation";
1046
      } else {
1047
        $css['transform'] = "rotate({$rotation}deg)";
1048
      }
1049 507
    }
1050
1051
    return $css;
1052
  }
1053
1054
  /**
1055 507
   * Create CSS style.
1056
   */
1057
  private function createCSSStyleFont(Font $font): array
1058 507
  {
1059
    // Construct CSS
1060
    $css = [];
1061 507
1062 15
    // Create CSS
1063
    if ($font->getBold()) {
1064 507
      $css['font-weight'] = 'bold';
1065 1
    }
1066 507
    if ($font->getUnderline() != Font::UNDERLINE_NONE && $font->getStrikethrough()) {
1067 10
      $css['text-decoration'] = 'underline line-through';
1068 507
    } elseif ($font->getUnderline() != Font::UNDERLINE_NONE) {
1069 1
      $css['text-decoration'] = 'underline';
1070
    } elseif ($font->getStrikethrough()) {
1071 507
      $css['text-decoration'] = 'line-through';
1072 9
    }
1073
    if ($font->getItalic()) {
1074
      $css['font-style'] = 'italic';
1075 507
    }
1076 507
1077 507
    $css['color'] = '#' . $font->getColor()->getRGB();
1078
    $css['font-family'] = '\'' . htmlspecialchars((string) $font->getName(), ENT_QUOTES) . '\'';
1079 507
    $css['font-size'] = $font->getSize() . 'pt';
1080
1081
    return $css;
1082
  }
1083
1084
  /**
1085
   * Create CSS style.
1086
   *
1087 507
   * @param Borders $borders Borders
1088
   */
1089
  private function createCSSStyleBorders(Borders $borders): array
1090 507
  {
1091
    // Construct CSS
1092
    $css = [];
1093 507
1094 492
    // Create CSS
1095 492
    if (!($this instanceof Pdf\Mpdf)) {
1096 492
      $css['border-bottom'] = $this->createCSSStyleBorder($borders->getBottom());
1097 492
      $css['border-top'] = $this->createCSSStyleBorder($borders->getTop());
1098
      $css['border-left'] = $this->createCSSStyleBorder($borders->getLeft());
1099
      $css['border-right'] = $this->createCSSStyleBorder($borders->getRight());
1100 21
    } else {
1101 5
      // Mpdf doesn't process !important, so omit unimportant border none
1102
      if ($borders->getBottom()->getBorderStyle() !== Border::BORDER_NONE) {
1103 21
        $css['border-bottom'] = $this->createCSSStyleBorder($borders->getBottom());
1104 5
      }
1105
      if ($borders->getTop()->getBorderStyle() !== Border::BORDER_NONE) {
1106 21
        $css['border-top'] = $this->createCSSStyleBorder($borders->getTop());
1107 5
      }
1108
      if ($borders->getLeft()->getBorderStyle() !== Border::BORDER_NONE) {
1109 21
        $css['border-left'] = $this->createCSSStyleBorder($borders->getLeft());
1110 5
      }
1111
      if ($borders->getRight()->getBorderStyle() !== Border::BORDER_NONE) {
1112
        $css['border-right'] = $this->createCSSStyleBorder($borders->getRight());
1113
      }
1114 507
    }
1115
1116
    return $css;
1117
  }
1118
1119
  /**
1120
   * Create CSS style.
1121
   *
1122 496
   * @param Border $border Border
1123
   */
1124
  private function createCSSStyleBorder(Border $border): string
1125 496
  {
1126
    //    Create CSS - add !important to non-none border styles for merged cells
1127 496
    $borderStyle = $this->mapBorderStyle($border->getBorderStyle());
1128
1129
    return $borderStyle . ' #' . $border->getColor()->getRGB() . (($borderStyle === self::BORDER_NONE) ? '' : ' !important');
1130
  }
1131
1132
  /**
1133
   * Create CSS style (Fill).
1134
   *
1135 507
   * @param Fill $fill Fill
1136
   */
1137
  private function createCSSStyleFill(Fill $fill): array
1138 507
  {
1139
    // Construct HTML
1140
    $css = [];
1141 507
1142 11
    // Create CSS
1143 11
    if ($fill->getFillType() !== Fill::FILL_NONE) {
1144 11
      $value = $fill->getFillType() == Fill::FILL_NONE
1145
        ? 'white' : '#' . $fill->getStartColor()->getRGB();
1146
      $css['background-color'] = $value;
1147 507
    }
1148
1149
    return $css;
1150
  }
1151
1152
  /**
1153 505
   * Generate HTML footer.
1154
   */
1155
  public function generateHTMLFooter(): string
1156 505
  {
1157 505
    // Construct HTML
1158 505
    $html = '';
1159
    $html .= '  </body>' . PHP_EOL;
1160 505
    $html .= '</html>' . PHP_EOL;
1161
1162
    return $html;
1163 12
  }
1164
1165 12
  private function generateTableTagInline(Worksheet $worksheet, string $id): string
1166 12
  {
1167
    $style = isset($this->cssStyles['table'])
1168 12
      ? $this->assembleCSS($this->cssStyles['table']) : '';
1169 12
1170 12
    $prntgrid = $worksheet->getPrintGridlines();
1171 1
    $viewgrid = $this->isPdf ? $prntgrid : $worksheet->getShowGridlines();
1172 12
    if ($viewgrid && $prntgrid) {
1173 6
      $html = "    <table border='1' cellpadding='1' $id cellspacing='1' style='$style' class='gridlines gridlinesp'>" . PHP_EOL;
1174 7
    } elseif ($viewgrid) {
1175 1
      $html = "    <table border='0' cellpadding='0' $id cellspacing='0' style='$style' class='gridlines'>" . PHP_EOL;
1176
    } elseif ($prntgrid) {
1177 7
      $html = "    <table border='0' cellpadding='0' $id cellspacing='0' style='$style' class='gridlinesp'>" . PHP_EOL;
1178
    } else {
1179
      $html = "    <table border='0' cellpadding='1' $id cellspacing='0' style='$style'>" . PHP_EOL;
1180 12
    }
1181
1182
    return $html;
1183 505
  }
1184
1185 505
  private function generateTableTag(Worksheet $worksheet, string $id, string &$html, int $sheetIndex): void
1186 498
  {
1187 498
    if (!$this->useInlineCss) {
1188 498
      $gridlines = $worksheet->getShowGridlines() ? ' gridlines' : '';
1189
      $gridlinesp = $worksheet->getPrintGridlines() ? ' gridlinesp' : '';
1190 12
      $html .= "    <table border='0' cellpadding='0' cellspacing='0' $id class='sheet$sheetIndex$gridlines$gridlinesp'>" . PHP_EOL;
1191
    } else {
1192
      $html .= $this->generateTableTagInline($worksheet, $id);
1193
    }
1194
  }
1195
1196
  /**
1197
   * Generate table header.
1198
   *
1199
   * @param Worksheet $worksheet The worksheet for the table we are writing
1200 505
   * @param bool $showid whether or not to add id to table tag
1201
   */
1202 505
  private function generateTableHeader(Worksheet $worksheet, bool $showid = true): string
1203
  {
1204
    $sheetIndex = $worksheet->getParentOrThrow()->getIndex($worksheet);
1205 505
1206 505
    // Construct HTML
1207 505
    $html = '';
1208 505
    $id = $showid ? "id='sheet$sheetIndex'" : '';
1209
    if ($showid) {
1210 2
      $html .= "<div style='page: page$sheetIndex'>" . PHP_EOL;
1211
    } else {
1212
      $html .= "<div style='page: page$sheetIndex' class='scrpgbrk'>" . PHP_EOL;
1213 505
    }
1214
1215
    $this->generateTableTag($worksheet, $id, $html, $sheetIndex);
1216 505
1217 505
    // Write <col> elements
1218 505
    $highestColumnIndex = Coordinate::columnIndexFromString($worksheet->getHighestColumn()) - 1;
1219 505
    $i = -1;
1220 498
    while ($i++ < $highestColumnIndex) {
1221
      if (!$this->useInlineCss) {
1222 12
        $html .= '        <col class="col' . $i . '" />' . PHP_EOL;
1223 12
      } else {
1224 12
        $style = isset($this->cssStyles['table.sheet' . $sheetIndex . ' col.col' . $i])
1225
          ? $this->assembleCSS($this->cssStyles['table.sheet' . $sheetIndex . ' col.col' . $i]) : '';
1226
        $html .= '        <col style="' . $style . '" />' . PHP_EOL;
1227
      }
1228 505
    }
1229
1230
    return $html;
1231
  }
1232
1233
  /**
1234 505
   * Generate table footer.
1235
   */
1236 505
  private function generateTableFooter(): string
1237
  {
1238
    return '    </tbody></table>' . PHP_EOL . '</div>' . PHP_EOL;
1239
  }
1240
1241
  /**
1242
   * Generate row start.
1243
   *
1244
   * @param int $sheetIndex Sheet index (0-based)
1245 505
   * @param int $row row number
1246
   */
1247 505
  private function generateRowStart(Worksheet $worksheet, int $sheetIndex, int $row): string
1248 505
  {
1249 2
    $html = '';
1250
    if (count($worksheet->getBreaks()) > 0) {
1251
      $breaks = $worksheet->getRowBreaks();
1252 2
1253
      // check if a break is needed before this row
1254 2
      if (isset($breaks['A' . $row])) {
1255 2
        // close table: </table>
1256 1
        $html .= $this->generateTableFooter();
1257
        if ($this->isPdf && $this->useInlineCss) {
1258
          $html .= '<div style="page-break-before:always" />';
1259
        }
1260 2
1261 2
        // open table again: <table> + <col> etc.
1262
        $html .= $this->generateTableHeader($worksheet, false);
1263
        $html .= '<tbody>' . PHP_EOL;
1264
      }
1265
    }
1266 505
1267 498
    // Write row start
1268
    if (!$this->useInlineCss) {
1269 12
      $html .= '          <tr class="row' . $row . '">' . PHP_EOL;
1270 12
    } else {
1271
      $style = isset($this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $row])
1272 12
        ? $this->assembleCSS($this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $row]) : '';
1273
1274
      $html .= '          <tr style="' . $style . '">' . PHP_EOL;
1275 505
    }
1276
1277
    return $html;
1278 505
  }
1279
1280 505
  private function generateRowCellCss(Worksheet $worksheet, string $cellAddress, int $row, int $columnNumber): array
1281 505
  {
1282 505
    $cell = ($cellAddress > '') ? $worksheet->getCellCollection()->get($cellAddress) : '';
1283 498
    $coordinate = Coordinate::stringFromColumnIndex($columnNumber + 1) . ($row + 1);
1284
    if (!$this->useInlineCss) {
1285 12
      $cssClass = 'column' . $columnNumber;
1286
    } else {
1287
      $cssClass = [];
1288
      // The statements below do nothing.
1289
      // Commenting out the code rather than deleting it
1290
      // in case someone can figure out what their intent was.
1291
      //if ($cellType == 'th') {
1292
      //    if (isset($this->cssStyles['table.sheet' . $sheetIndex . ' th.column' . $colNum])) {
1293
      //        $this->cssStyles['table.sheet' . $sheetIndex . ' th.column' . $colNum];
1294
      //    }
1295
      //} else {
1296
      //    if (isset($this->cssStyles['table.sheet' . $sheetIndex . ' td.column' . $colNum])) {
1297
      //        $this->cssStyles['table.sheet' . $sheetIndex . ' td.column' . $colNum];
1298
      //    }
1299
      //}
1300
      // End of mystery statements.
1301 505
    }
1302
1303
    return [$cell, $cssClass, $coordinate];
1304 30
  }
1305
1306 30
  private function generateRowCellDataValueRich(RichText $richText): string
1307
  {
1308 30
    $cellData = '';
1309 30
    // Loop through rich text elements
1310
    $elements = $richText->getRichTextElements();
1311 30
    foreach ($elements as $element) {
1312 14
      // Rich text start?
1313 14
      if ($element instanceof Run) {
1314 14
        $cellEnd = '';
1315
        if ($element->getFont() !== null) {
1316 14
          $cellData .= '<span style="' . $this->assembleCSS($this->createCSSStyleFont($element->getFont())) . '">';
1317 1
1318 1
          if ($element->getFont()->getSuperscript()) {
1319 14
            $cellData .= '<sup>';
1320 1
            $cellEnd = '</sup>';
1321 1
          } elseif ($element->getFont()->getSubscript()) {
1322
            $cellData .= '<sub>';
1323
            $cellEnd = '</sub>';
1324
          }
1325
        } else {
1326
          $cellData .= '<span>';
1327
        }
1328 14
1329 14
        // Convert UTF8 data to PCDATA
1330
        $cellText = $element->getText();
1331 14
        $cellData .= htmlspecialchars($cellText, Settings::htmlEntityFlags());
1332
1333 14
        $cellData .= $cellEnd;
1334
1335
        $cellData .= '</span>';
1336 26
      } else {
1337 26
        // Convert UTF8 data to PCDATA
1338
        $cellText = $element->getText();
1339
        $cellData .= htmlspecialchars($cellText, Settings::htmlEntityFlags());
1340
      }
1341 30
    }
1342
1343
    return nl2br($cellData);
1344 503
  }
1345
1346 503
  private function generateRowCellDataValue(Worksheet $worksheet, Cell $cell, string &$cellData): void
1347 10
  {
1348
    if ($cell->getValue() instanceof RichText) {
1349 503
      $cellData .= $this->generateRowCellDataValueRich($cell->getValue());
1350 503
    } else {
1351 503
      $origData = $this->preCalculateFormulas ? $cell->getCalculatedValue() : $cell->getValue();
1352
      $origData2 = $this->preCalculateFormulas ? $cell->getCalculatedValueString() : $cell->getValueString();
1353 503
      $formatCode = $worksheet->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex())->getNumberFormat()->getFormatCode();
1354 503
1355 503
      $cellData = NumberFormat::toFormattedString(
1356 503
        $origData2,
1357 503
        $formatCode ?? NumberFormat::FORMAT_GENERAL,
1358
        [$this, 'formatColor']
1359 503
      );
1360 88
1361
      if ($cellData === $origData) {
1362 503
        $cellData = htmlspecialchars($cellData, Settings::htmlEntityFlags());
1363 1
      }
1364 503
      if ($worksheet->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex())->getFont()->getSuperscript()) {
1365 1
        $cellData = '<sup>' . $cellData . '</sup>';
1366
      } elseif ($worksheet->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex())->getFont()->getSubscript()) {
1367
        $cellData = '<sub>' . $cellData . '</sub>';
1368
      }
1369
    }
1370 505
  }
1371
1372 505
  private function generateRowCellData(Worksheet $worksheet, null|Cell|string $cell, array|string &$cssClass): string
1373 505
  {
1374 503
    $cellData = '&nbsp;';
1375
    if ($cell instanceof Cell) {
1376
      $cellData = '';
1377
      // Don't know what this does, and no test cases.
1378
      //if ($cell->getParent() === null) {
1379
      //    $cell->attach($worksheet);
1380 503
      //}
1381
      // Value
1382
      $this->generateRowCellDataValue($worksheet, $cell, $cellData);
1383
1384 503
      // Converts the cell content so that spaces occuring at beginning of each new line are replaced by &nbsp;
1385
      // Example: "  Hello\n to the world" is converted to "&nbsp;&nbsp;Hello\n&nbsp;to the world"
1386
      $cellData = (string) preg_replace('/(?m)(?:^|\\G) /', '&nbsp;', $cellData);
1387 503
1388
      // convert newline "\n" to '<br>'
1389
      $cellData = nl2br($cellData);
1390 503
1391 497
      // Extend CSS class?
1392 497
      if (!$this->useInlineCss && is_string($cssClass)) {
1393 11
        $cssClass .= ' style' . $cell->getXfIndex();
1394 11
        $cssClass .= ' ' . $cell->getDataType();
1395 11
      } elseif (is_array($cssClass)) {
1396 11
        $index = $cell->getXfIndex();
1397 11
        $styleIndex = 'td.style' . $index . ', th.style' . $index;
1398
        if (isset($this->cssStyles[$styleIndex])) {
1399
          $cssClass = array_merge($cssClass, $this->cssStyles[$styleIndex]);
1400
        }
1401 11
1402
        // General horizontal alignment: Actual horizontal alignment depends on dataType
1403 11
        $sharedStyle = $worksheet->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex());
1404 11
        if (
1405
          $sharedStyle->getAlignment()->getHorizontal() == Alignment::HORIZONTAL_GENERAL
1406 11
          && isset($this->cssStyles['.' . $cell->getDataType()]['text-align'])
1407
        ) {
1408
          $cssClass['text-align'] = $this->cssStyles['.' . $cell->getDataType()]['text-align'];
1409
        }
1410
      }
1411 47
    } else {
1412 43
      // Use default borders for empty cell
1413
      if (is_string($cssClass)) {
1414
        $cssClass .= ' style0';
1415
      }
1416 505
    }
1417
1418
    return $cellData;
1419 505
  }
1420
1421 505
  private function generateRowIncludeCharts(Worksheet $worksheet, string $coordinate): string
1422
  {
1423
    return $this->includeCharts ? $this->writeChartInCell($worksheet, $coordinate) : '';
1424 505
  }
1425
1426 505
  private function generateRowSpans(string $html, int $rowSpan, int $colSpan): string
1427 505
  {
1428
    $html .= ($colSpan > 1) ? (' colspan="' . $colSpan . '"') : '';
1429 505
    $html .= ($rowSpan > 1) ? (' rowspan="' . $rowSpan . '"') : '';
1430
1431
    return $html;
1432 505
  }
1433
1434
  private function generateRowWriteCell(
1435
    string &$html,
1436
    Worksheet $worksheet,
1437
    string $coordinate,
1438
    string $cellType,
1439
    string $cellData,
1440
    int $colSpan,
1441
    int $rowSpan,
1442
    array|string $cssClass,
1443
    int $colNum,
1444
    int $sheetIndex,
1445
    int $row
1446 505
  ): void {
1447
    // Image?
1448 505
    $htmlx = $this->writeImageInCell($coordinate);
1449
    // Chart?
1450 505
    $htmlx .= $this->generateRowIncludeCharts($worksheet, $coordinate);
1451 505
    // Column start
1452 476
    $html .= '            <' . $cellType;
1453 476
    if (!$this->useInlineCss && !$this->isPdf && is_string($cssClass)) {
1454 22
      $html .= ' class="' . $cssClass . '"';
1455
      if ($htmlx) {
1456
        $html .= " style='position: relative;'";
1457
      }
1458
    } else {
1459
      //** Necessary redundant code for the sake of \PhpOffice\PhpSpreadsheet\Writer\Pdf **
1460 38
      // We must explicitly write the width of the <td> element because TCPDF
1461 12
      // does not recognize e.g. <col style="width:42pt">
1462
      if ($this->useInlineCss) {
1463 27
        $xcssClass = is_array($cssClass) ? $cssClass : [];
1464 27
      } else {
1465
        if (is_string($cssClass)) {
1466 27
          $html .= ' class="' . $cssClass . '"';
1467
        }
1468 38
        $xcssClass = [];
1469 38
      }
1470 38
      $width = 0;
1471 38
      $i = $colNum - 1;
1472 38
      $e = $colNum + $colSpan - 1;
1473 38
      while ($i++ < $e) {
1474
        if (isset($this->columnWidths[$sheetIndex][$i])) {
1475
          $width += $this->columnWidths[$sheetIndex][$i];
1476 38
        }
1477
      }
1478
      $xcssClass['width'] = (string) $width . 'pt';
1479 38
      // We must also explicitly write the height of the <td> element because TCPDF
1480 5
      // does not recognize e.g. <tr style="height:50pt">
1481 5
      if (isset($this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $row]['height'])) {
1482
        $height = $this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $row]['height'];
1483
        $xcssClass['height'] = $height;
1484 38
      }
1485 12
      //** end of redundant code **
1486 12
      if ($this->useInlineCss) {
1487 11
        foreach (['border-top', 'border-bottom', 'border-right', 'border-left'] as $borderType) {
1488
          if (($xcssClass[$borderType] ?? '') === 'none #000000') {
1489
            unset($xcssClass[$borderType]);
1490
          }
1491
        }
1492 38
      }
1493 10
1494
      if ($htmlx) {
1495 38
        $xcssClass['position'] = 'relative';
1496 38
      }
1497 12
      $html .= ' style="' . $this->assembleCSS($xcssClass) . '"';
1498
      if ($this->useInlineCss) {
1499
        $html .= ' class="gridlines gridlinesp"';
1500 505
      }
1501
    }
1502 505
    $html = $this->generateRowSpans($html, $rowSpan, $colSpan);
1503 505
1504
    $html .= '>';
1505 505
    $html .= $htmlx;
1506
1507
    $html .= $this->writeComment($worksheet, $coordinate);
1508 505
1509
    // Cell data
1510
    $html .= $cellData;
1511 505
1512
    // Column end
1513
    $html .= '</' . $cellType . '>' . PHP_EOL;
1514
  }
1515
1516
  /**
1517
   * Generate row.
1518
   *
1519
   * @param array $values Array containing cells in a row
1520
   * @param int $row Row number (0-based)
1521 505
   * @param string $cellType eg: 'td'
1522
   */
1523
  private function generateRow(Worksheet $worksheet, array $values, int $row, string $cellType): string
1524 505
  {
1525 505
    // Sheet index
1526
    $sheetIndex = $worksheet->getParentOrThrow()->getIndex($worksheet);
1527
    $html = $this->generateRowStart($worksheet, $sheetIndex, $row);
1528 505
1529 505
    // Write cells
1530 505
    $colNum = 0;
1531
    foreach ($values as $cellAddress) {
1532
      [$cell, $cssClass, $coordinate] = $this->generateRowCellCss($worksheet, $cellAddress, $row, $colNum);
1533 505
1534
      // Cell Data
1535
      $cellData = $this->generateRowCellData($worksheet, $cell, $cssClass);
1536 505
1537 10
      // Hyperlink?
1538 10
      if ($worksheet->hyperlinkExists($coordinate) && !$worksheet->getHyperlink($coordinate)->isInternal()) {
1539 10
        $url = $worksheet->getHyperlink($coordinate)->getUrl();
1540 10
        $urldecode = strtolower(html_entity_decode(trim($url), encoding: 'UTF-8'));
1541 1
        $parseScheme = preg_match('/^(\\w+):/', $urldecode, $matches);
1542
        if ($parseScheme === 1 && !in_array($matches[1], ['http', 'https', 'file', 'ftp', 's3'], true)) {
1543 10
          $cellData = htmlspecialchars($url, Settings::htmlEntityFlags());
1544
        } else {
1545
          $cellData = '<a href="' . htmlspecialchars($url, Settings::htmlEntityFlags()) . '" title="' . htmlspecialchars($worksheet->getHyperlink($coordinate)->getTooltip(), Settings::htmlEntityFlags()) . '">' . $cellData . '</a>';
1546
        }
1547
      }
1548 505
1549 505
      // Should the cell be written or is it swallowed by a rowspan or colspan?
1550
      $writeCell = !(isset($this->isSpannedCell[$worksheet->getParentOrThrow()->getIndex($worksheet)][$row + 1][$colNum])
1551
        && $this->isSpannedCell[$worksheet->getParentOrThrow()->getIndex($worksheet)][$row + 1][$colNum]);
1552 505
1553 505
      // Colspan and Rowspan
1554 505
      $colSpan = 1;
1555 15
      $rowSpan = 1;
1556 15
      if (isset($this->isBaseCell[$worksheet->getParentOrThrow()->getIndex($worksheet)][$row + 1][$colNum])) {
1557 15
        $spans = $this->isBaseCell[$worksheet->getParentOrThrow()->getIndex($worksheet)][$row + 1][$colNum];
1558
        $rowSpan = $spans['rowspan'];
1559
        $colSpan = $spans['colspan'];
1560
1561 15
        //    Also apply style from last cell in merge to fix borders -
1562 15
        //        relies on !important for non-none border declarations in createCSSStyleBorder
1563 13
        $endCellCoord = Coordinate::stringFromColumnIndex($colNum + $colSpan) . ($row + $rowSpan);
1564
        if (!$this->useInlineCss) {
1565 3
          $cssClass .= ' style' . $worksheet->getCell($endCellCoord)->getXfIndex();
1566 3
        } else {
1567 3
          $endBorders = $this->spreadsheet->getCellXfByIndex($worksheet->getCell($endCellCoord)->getXfIndex())->getBorders();
1568 3
          $altBorders = $this->createCSSStyleBorders($endBorders);
1569 1
          foreach ($altBorders as $altKey => $altValue) {
1570
            if (str_contains($altValue, '!important')) {
1571
              $cssClass[$altKey] = $altValue;
1572
            }
1573
          }
1574
        }
1575
      }
1576 505
1577 505
      // Write
1578
      if ($writeCell) {
1579
        $this->generateRowWriteCell($html, $worksheet, $coordinate, $cellType, $cellData, $colSpan, $rowSpan, $cssClass, $colNum, $sheetIndex, $row);
1580
      }
1581 505
1582
      // Next column
1583
      ++$colNum;
1584
    }
1585 505
1586
    // Write row end
1587
    $html .= '          </tr>' . PHP_EOL;
1588 505
1589
    // Return
1590
    return $html;
1591
  }
1592
1593
  /**
1594 507
   * Takes array where of CSS properties / values and converts to CSS string.
1595
   */
1596 507
  private function assembleCSS(array $values = []): string
1597 507
  {
1598 507
    $pairs = [];
1599
    foreach ($values as $property => $value) {
1600 507
      $pairs[] = $property . ':' . $value;
1601
    }
1602 507
    $string = implode('; ', $pairs);
1603
1604
    return $string;
1605
  }
1606
1607
  /**
1608 17
   * Get images root.
1609
   */
1610 17
  public function getImagesRoot(): string
1611
  {
1612
    return $this->imagesRoot;
1613
  }
1614
1615
  /**
1616
   * Set images root.
1617
   *
1618 1
   * @return $this
1619
   */
1620 1
  public function setImagesRoot(string $imagesRoot): static
1621
  {
1622 1
    $this->imagesRoot = $imagesRoot;
1623
1624
    return $this;
1625
  }
1626
1627
  /**
1628 5
   * Get embed images.
1629
   */
1630 5
  public function getEmbedImages(): bool
1631
  {
1632
    return $this->embedImages;
1633
  }
1634
1635
  /**
1636
   * Set embed images.
1637
   *
1638 4
   * @return $this
1639
   */
1640 4
  public function setEmbedImages(bool $embedImages): static
1641
  {
1642 4
    $this->embedImages = $embedImages;
1643
1644
    return $this;
1645
  }
1646
1647
  /**
1648 1
   * Get use inline CSS?
1649
   */
1650 1
  public function getUseInlineCss(): bool
1651
  {
1652
    return $this->useInlineCss;
1653
  }
1654
1655
  /**
1656
   * Set use inline CSS?
1657
   *
1658 13
   * @return $this
1659
   */
1660 13
  public function setUseInlineCss(bool $useInlineCss): static
1661
  {
1662 13
    $this->useInlineCss = $useInlineCss;
1663
1664
    return $this;
1665
  }
1666
1667
  /**
1668
   * Add color to formatted string as inline style.
1669
   *
1670
   * @param string $value Plain formatted value without color
1671 392
   * @param string $format Format code
1672
   */
1673
  public function formatColor(string $value, string $format): string
1674 392
  {
1675 392
    // Color information, e.g. [Red] is always at the beginning
1676
    $color = null; // initialize
1677 392
    $matches = [];
1678 392
1679 17
    $color_regex = '/^\\[[a-zA-Z]+\\]/';
1680 17
    if (preg_match($color_regex, $format, $matches)) {
1681
      $color = str_replace(['[', ']'], '', $matches[0]);
1682
      $color = strtolower($color);
1683
    }
1684 392
1685
    // convert to PCDATA
1686
    $result = htmlspecialchars($value, Settings::htmlEntityFlags());
1687 392
1688 17
    // color span tag
1689
    if ($color !== null) {
1690
      $result = '<span style="color:' . $color . '">' . $result . '</span>';
1691 392
    }
1692
1693
    return $result;
1694
  }
1695
1696
  /**
1697 507
   * Calculate information about HTML colspan and rowspan which is not always the same as Excel's.
1698
   */
1699 507
  private function calculateSpans(): void
1700 507
  {
1701
    if ($this->spansAreCalculated) {
1702
      return;
1703
    }
1704
    // Identify all cells that should be omitted in HTML due to cell merge.
1705 507
    // In HTML only the upper-left cell should be written and it should have
1706 507
    //   appropriate rowspan / colspan attribute
1707
    if (is_array($this->sheetIndex)) {
1708 507
      $sheetIndexes = $this->sheetIndex;
1709 507
    } else {
1710
      $sheetIndexes = $this->sheetIndex !== null
1711 507
        ? [$this->sheetIndex] : range(0, $this->spreadsheet->getSheetCount() - 1);
1712
    }
1713
1714 507
    foreach ($sheetIndexes as $sheetIndex) {
1715 15
      $sheet = $this->spreadsheet->getSheet($sheetIndex);
1716 15
1717 15
      $candidateSpannedRow = [];
1718
1719 15
      // loop through all Excel merged cells
1720 15
      foreach ($sheet->getMergeCells() as $cells) {
1721
        [$cells] = Coordinate::splitRange($cells);
1722 15
        $first = $cells[0];
1723 15
        $last = $cells[1];
1724
1725
        [$fc, $fr] = Coordinate::indexesFromString($first);
1726 15
        $fc = $fc - 1;
1727 15
1728
        [$lc, $lr] = Coordinate::indexesFromString($last);
1729 15
        $lc = $lc - 1;
1730
1731 15
        // loop through the individual cells in the individual merge
1732 15
        $r = $fr - 1;
1733 15
        while ($r++ < $lr) {
1734
          // also, flag this row as a HTML row that is candidate to be omitted
1735 15
          $candidateSpannedRow[$r] = $r;
1736 15
1737 15
          $c = $fc - 1;
1738
          while ($c++ < $lc) {
1739
            if (!($c == $fc && $r == $fr)) {
1740 15
              // not the upper-left cell (should not be written in HTML)
1741 15
              $this->isSpannedCell[$sheetIndex][$r][$c] = [
1742 15
                'baseCell' => [$fr, $fc],
1743 15
              ];
1744 15
            } else {
1745 15
              // upper-left is the base cell that should hold the colspan/rowspan attribute
1746
              $this->isBaseCell[$sheetIndex][$r][$c] = [
1747
                'xlrowspan' => $lr - $fr + 1, // Excel rowspan
1748
                'rowspan' => $lr - $fr + 1, // HTML rowspan, value may change
1749
                'xlcolspan' => $lc - $fc + 1, // Excel colspan
1750
                'colspan' => $lc - $fc + 1, // HTML colspan, value may change
1751 507
              ];
1752
            }
1753
          }
1754
        }
1755
      }
1756
1757 507
      $this->calculateSpansOmitRows($sheet, $sheetIndex, $candidateSpannedRow);
1758
1759
      // TODO: Same for columns
1760 507
    }
1761
1762
    // We have calculated the spans
1763
    $this->spansAreCalculated = true;
1764 507
  }
1765 507
1766 15
  private function calculateSpansOmitRows(Worksheet $sheet, int $sheetIndex, array $candidateSpannedRow): void
1767 15
  {
1768 8
    // Identify which rows should be omitted in HTML. These are the rows where all the cells
1769
    //   participate in a merge and the where base cells are somewhere above.
1770
    $countColumns = Coordinate::columnIndexFromString($sheet->getHighestColumn());
1771
    foreach ($candidateSpannedRow as $rowIndex) {
1772
      if (isset($this->isSpannedCell[$sheetIndex][$rowIndex])) {
1773
        if (count($this->isSpannedCell[$sheetIndex][$rowIndex]) == $countColumns) {
1774 507
          $this->isSpannedRow[$sheetIndex][$rowIndex] = $rowIndex;
1775 8
        }
1776 8
      }
1777 8
    }
1778 8
1779 8
    // For each of the omitted rows we found above, the affected rowspans should be subtracted by 1
1780 8
    if (isset($this->isSpannedRow[$sheetIndex])) {
1781
      foreach ($this->isSpannedRow[$sheetIndex] as $rowIndex) {
1782 8
        $adjustedBaseCells = [];
1783
        $c = -1;
1784 8
        $e = $countColumns - 1;
1785 8
        while ($c++ < $e) {
1786
          $baseCell = $this->isSpannedCell[$sheetIndex][$rowIndex][$c]['baseCell'];
1787
1788
          if (!in_array($baseCell, $adjustedBaseCells, true)) {
1789
            // subtract rowspan by 1
1790
            --$this->isBaseCell[$sheetIndex][$baseCell[0]][$baseCell[1]]['rowspan'];
1791
            $adjustedBaseCells[] = $baseCell;
1792
          }
1793
        }
1794
      }
1795
    }
1796
  }
1797 505
1798
  /**
1799 505
   * Write a comment in the same format as LibreOffice.
1800 505
   *
1801 23
   * @see https://github.com/LibreOffice/core/blob/9fc9bf3240f8c62ad7859947ab8a033ac1fe93fa/sc/source/filter/html/htmlexp.cxx#L1073-L1092
1802 23
   */
1803 23
  private function writeComment(Worksheet $worksheet, string $coordinate): string
1804 23
  {
1805 23
    $result = '';
1806 2
    if (!$this->isPdf && isset($worksheet->getComments()[$coordinate])) {
1807
      $sanitizedString = $this->generateRowCellDataValueRich($worksheet->getComment($coordinate)->getText());
1808 23
      $dir = ($worksheet->getComment($coordinate)->getTextboxDirection() === Comment::TEXTBOX_DIRECTION_RTL) ? ' dir="rtl"' : '';
1809 23
      $align = strtolower($worksheet->getComment($coordinate)->getAlignment());
1810 23
      $alignment = Alignment::HORIZONTAL_ALIGNMENT_FOR_HTML[$align] ?? '';
1811 23
      if ($alignment !== '') {
1812
        $alignment = " style=\"text-align:$alignment\"";
1813
      }
1814
      if ($sanitizedString !== '') {
1815 505
        $result .= '<a class="comment-indicator"></a>';
1816
        $result .= "<div class=\"comment\"$dir$alignment>" . $sanitizedString . '</div>';
1817
        $result .= PHP_EOL;
1818 480
      }
1819
    }
1820
1821 480
    return $result;
1822
  }
1823
1824
  public function getOrientation(): ?string
1825
  {
1826
    // Expect Pdf classes to override this method.
1827 507
    return $this->isPdf ? PageSetup::ORIENTATION_PORTRAIT : null;
1828
  }
1829
1830 507
  /**
1831
   * Generate @page declarations.
1832
   */
1833 507
  private function generatePageDeclarations(bool $generateSurroundingHTML): string
1834 507
  {
1835 14
    // Ensure that Spans have been calculated?
1836
    $this->calculateSpans();
1837 500
1838
    // Fetch sheets
1839
    $sheets = [];
1840
    if (is_array($this->sheetIndex)) {
1841 507
      foreach ($this->sheetIndex as $sheetIndex) {
1842
        $sheets[] = $this->spreadsheet->getSheet($sheetIndex);
1843
      }
1844 507
    } elseif ($this->sheetIndex === null) {
1845 507
      $sheets = $this->spreadsheet->getAllSheets();
1846 507
    } else {
1847 507
      $sheets[] = $this->spreadsheet->getSheet($this->sheetIndex);
1848 507
    }
1849 507
1850 507
    // Construct HTML
1851 507
    $htmlPage = $generateSurroundingHTML ? ('<style type="text/css">' . PHP_EOL) : '';
1852 507
1853 507
    // Loop all sheets
1854 507
    $sheetId = 0;
1855 507
    foreach ($sheets as $worksheet) {
1856 507
      $htmlPage .= "@page page$sheetId { ";
1857 9
      $left = StringHelper::formatNumber($worksheet->getPageMargins()->getLeft()) . 'in; ';
1858 499
      $htmlPage .= 'margin-left: ' . $left;
1859 12
      $right = StringHelper::FormatNumber($worksheet->getPageMargins()->getRight()) . 'in; ';
1860
      $htmlPage .= 'margin-right: ' . $right;
1861 507
      $top = StringHelper::FormatNumber($worksheet->getPageMargins()->getTop()) . 'in; ';
1862 507
      $htmlPage .= 'margin-top: ' . $top;
1863
      $bottom = StringHelper::FormatNumber($worksheet->getPageMargins()->getBottom()) . 'in; ';
1864 507
      $htmlPage .= 'margin-bottom: ' . $bottom;
1865 507
      $orientation = $this->getOrientation() ?? $worksheet->getPageSetup()->getOrientation();
1866 507
      if ($orientation === PageSetup::ORIENTATION_LANDSCAPE) {
1867 507
        $htmlPage .= 'size: landscape; ';
1868 507
      } elseif ($orientation === PageSetup::ORIENTATION_PORTRAIT) {
1869 507
        $htmlPage .= 'size: portrait; ';
1870 507
      }
1871 507
      $htmlPage .= '}' . PHP_EOL;
1872 507
      ++$sheetId;
1873 507
    }
1874 507
    $htmlPage .= implode(PHP_EOL, [
1875 507
      '.navigation {page-break-after: always;}',
1876 507
      '.scrpgbrk, div + div {page-break-before: always;}',
1877 507
      '@media screen {',
1878 507
      '  .gridlines td {border: 1px solid black;}',
1879 507
      '  .gridlines th {border: 1px solid black;}',
1880 507
      '  body>div {margin-top: 5px;}',
1881 507
      '  body>div:first-child {margin-top: 0;}',
1882
      '  .scrpgbrk {margin-top: 1px;}',
1883 507
      '}',
1884
      '@media print {',
1885
      '  .gridlinesp td {border: 1px solid black;}',
1886 505
      '  .gridlinesp th {border: 1px solid black;}',
1887
      '  .navigation {display: none;}',
1888 505
      '}',
1889 485
      '',
1890
    ]);
1891
    $htmlPage .= $generateSurroundingHTML ? ('</style>' . PHP_EOL) : '';
1892 26
1893
    return $htmlPage;
1894
  }
1895 507
1896
  private function shouldGenerateRow(Worksheet $sheet, int $row): bool
1897 507
  {
1898 487
    if (!($this instanceof Pdf\Mpdf || $this instanceof Pdf\Tcpdf)) {
1899
      return true;
1900 26
    }
1901 25
1902
    return $sheet->isRowVisible($row);
1903
  }
1904 9
1905
  private function shouldGenerateColumn(Worksheet $sheet, string $colStr): bool
1906
  {
1907
    if (!($this instanceof Pdf\Mpdf || $this instanceof Pdf\Tcpdf)) {
1908
      return true;
1909
    }
1910
    if (!$sheet->columnDimensionExists($colStr)) {
1911
      return true;
1912
    }
1913
1914
    return $sheet->getColumnDimension($colStr)->getVisible();
1915
  }
1916
}
1917