Html.php 57 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683
  1. <?php
  2. namespace PhpOffice\PhpSpreadsheet\Writer;
  3. use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
  4. use PhpOffice\PhpSpreadsheet\Cell\Cell;
  5. use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
  6. use PhpOffice\PhpSpreadsheet\Chart\Chart;
  7. use PhpOffice\PhpSpreadsheet\RichText\RichText;
  8. use PhpOffice\PhpSpreadsheet\RichText\Run;
  9. use PhpOffice\PhpSpreadsheet\Shared\Drawing as SharedDrawing;
  10. use PhpOffice\PhpSpreadsheet\Shared\File;
  11. use PhpOffice\PhpSpreadsheet\Shared\Font as SharedFont;
  12. use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
  13. use PhpOffice\PhpSpreadsheet\Spreadsheet;
  14. use PhpOffice\PhpSpreadsheet\Style\Alignment;
  15. use PhpOffice\PhpSpreadsheet\Style\Border;
  16. use PhpOffice\PhpSpreadsheet\Style\Borders;
  17. use PhpOffice\PhpSpreadsheet\Style\Fill;
  18. use PhpOffice\PhpSpreadsheet\Style\Font;
  19. use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
  20. use PhpOffice\PhpSpreadsheet\Style\Style;
  21. use PhpOffice\PhpSpreadsheet\Worksheet\Drawing;
  22. use PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing;
  23. use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
  24. use PhpOffice\PhpSpreadsheet\Writer\Exception as WriterException;
  25. class Html extends BaseWriter
  26. {
  27. /**
  28. * Spreadsheet object.
  29. *
  30. * @var Spreadsheet
  31. */
  32. protected $spreadsheet;
  33. /**
  34. * Sheet index to write.
  35. *
  36. * @var int
  37. */
  38. private $sheetIndex = 0;
  39. /**
  40. * Images root.
  41. *
  42. * @var string
  43. */
  44. private $imagesRoot = '';
  45. /**
  46. * embed images, or link to images.
  47. *
  48. * @var bool
  49. */
  50. private $embedImages = false;
  51. /**
  52. * Use inline CSS?
  53. *
  54. * @var bool
  55. */
  56. private $useInlineCss = false;
  57. /**
  58. * Array of CSS styles.
  59. *
  60. * @var array
  61. */
  62. private $cssStyles;
  63. /**
  64. * Array of column widths in points.
  65. *
  66. * @var array
  67. */
  68. private $columnWidths;
  69. /**
  70. * Default font.
  71. *
  72. * @var Font
  73. */
  74. private $defaultFont;
  75. /**
  76. * Flag whether spans have been calculated.
  77. *
  78. * @var bool
  79. */
  80. private $spansAreCalculated = false;
  81. /**
  82. * Excel cells that should not be written as HTML cells.
  83. *
  84. * @var array
  85. */
  86. private $isSpannedCell = [];
  87. /**
  88. * Excel cells that are upper-left corner in a cell merge.
  89. *
  90. * @var array
  91. */
  92. private $isBaseCell = [];
  93. /**
  94. * Excel rows that should not be written as HTML rows.
  95. *
  96. * @var array
  97. */
  98. private $isSpannedRow = [];
  99. /**
  100. * Is the current writer creating PDF?
  101. *
  102. * @var bool
  103. */
  104. protected $isPdf = false;
  105. /**
  106. * Generate the Navigation block.
  107. *
  108. * @var bool
  109. */
  110. private $generateSheetNavigationBlock = true;
  111. /**
  112. * Create a new HTML.
  113. *
  114. * @param Spreadsheet $spreadsheet
  115. */
  116. public function __construct(Spreadsheet $spreadsheet)
  117. {
  118. $this->spreadsheet = $spreadsheet;
  119. $this->defaultFont = $this->spreadsheet->getDefaultStyle()->getFont();
  120. }
  121. /**
  122. * Save Spreadsheet to file.
  123. *
  124. * @param string $pFilename
  125. *
  126. * @throws WriterException
  127. */
  128. public function save($pFilename)
  129. {
  130. // garbage collect
  131. $this->spreadsheet->garbageCollect();
  132. $saveDebugLog = Calculation::getInstance($this->spreadsheet)->getDebugLog()->getWriteDebugLog();
  133. Calculation::getInstance($this->spreadsheet)->getDebugLog()->setWriteDebugLog(false);
  134. $saveArrayReturnType = Calculation::getArrayReturnType();
  135. Calculation::setArrayReturnType(Calculation::RETURN_ARRAY_AS_VALUE);
  136. // Build CSS
  137. $this->buildCSS(!$this->useInlineCss);
  138. // Open file
  139. $fileHandle = fopen($pFilename, 'wb+');
  140. if ($fileHandle === false) {
  141. throw new WriterException("Could not open file $pFilename for writing.");
  142. }
  143. // Write headers
  144. fwrite($fileHandle, $this->generateHTMLHeader(!$this->useInlineCss));
  145. // Write navigation (tabs)
  146. if ((!$this->isPdf) && ($this->generateSheetNavigationBlock)) {
  147. fwrite($fileHandle, $this->generateNavigation());
  148. }
  149. // Write data
  150. fwrite($fileHandle, $this->generateSheetData());
  151. // Write footer
  152. fwrite($fileHandle, $this->generateHTMLFooter());
  153. // Close file
  154. fclose($fileHandle);
  155. Calculation::setArrayReturnType($saveArrayReturnType);
  156. Calculation::getInstance($this->spreadsheet)->getDebugLog()->setWriteDebugLog($saveDebugLog);
  157. }
  158. /**
  159. * Map VAlign.
  160. *
  161. * @param string $vAlign Vertical alignment
  162. *
  163. * @return string
  164. */
  165. private function mapVAlign($vAlign)
  166. {
  167. switch ($vAlign) {
  168. case Alignment::VERTICAL_BOTTOM:
  169. return 'bottom';
  170. case Alignment::VERTICAL_TOP:
  171. return 'top';
  172. case Alignment::VERTICAL_CENTER:
  173. case Alignment::VERTICAL_JUSTIFY:
  174. return 'middle';
  175. default:
  176. return 'baseline';
  177. }
  178. }
  179. /**
  180. * Map HAlign.
  181. *
  182. * @param string $hAlign Horizontal alignment
  183. *
  184. * @return false|string
  185. */
  186. private function mapHAlign($hAlign)
  187. {
  188. switch ($hAlign) {
  189. case Alignment::HORIZONTAL_GENERAL:
  190. return false;
  191. case Alignment::HORIZONTAL_LEFT:
  192. return 'left';
  193. case Alignment::HORIZONTAL_RIGHT:
  194. return 'right';
  195. case Alignment::HORIZONTAL_CENTER:
  196. case Alignment::HORIZONTAL_CENTER_CONTINUOUS:
  197. return 'center';
  198. case Alignment::HORIZONTAL_JUSTIFY:
  199. return 'justify';
  200. default:
  201. return false;
  202. }
  203. }
  204. /**
  205. * Map border style.
  206. *
  207. * @param int $borderStyle Sheet index
  208. *
  209. * @return string
  210. */
  211. private function mapBorderStyle($borderStyle)
  212. {
  213. switch ($borderStyle) {
  214. case Border::BORDER_NONE:
  215. return 'none';
  216. case Border::BORDER_DASHDOT:
  217. return '1px dashed';
  218. case Border::BORDER_DASHDOTDOT:
  219. return '1px dotted';
  220. case Border::BORDER_DASHED:
  221. return '1px dashed';
  222. case Border::BORDER_DOTTED:
  223. return '1px dotted';
  224. case Border::BORDER_DOUBLE:
  225. return '3px double';
  226. case Border::BORDER_HAIR:
  227. return '1px solid';
  228. case Border::BORDER_MEDIUM:
  229. return '2px solid';
  230. case Border::BORDER_MEDIUMDASHDOT:
  231. return '2px dashed';
  232. case Border::BORDER_MEDIUMDASHDOTDOT:
  233. return '2px dotted';
  234. case Border::BORDER_MEDIUMDASHED:
  235. return '2px dashed';
  236. case Border::BORDER_SLANTDASHDOT:
  237. return '2px dashed';
  238. case Border::BORDER_THICK:
  239. return '3px solid';
  240. case Border::BORDER_THIN:
  241. return '1px solid';
  242. default:
  243. // map others to thin
  244. return '1px solid';
  245. }
  246. }
  247. /**
  248. * Get sheet index.
  249. *
  250. * @return int
  251. */
  252. public function getSheetIndex()
  253. {
  254. return $this->sheetIndex;
  255. }
  256. /**
  257. * Set sheet index.
  258. *
  259. * @param int $pValue Sheet index
  260. *
  261. * @return HTML
  262. */
  263. public function setSheetIndex($pValue)
  264. {
  265. $this->sheetIndex = $pValue;
  266. return $this;
  267. }
  268. /**
  269. * Get sheet index.
  270. *
  271. * @return bool
  272. */
  273. public function getGenerateSheetNavigationBlock()
  274. {
  275. return $this->generateSheetNavigationBlock;
  276. }
  277. /**
  278. * Set sheet index.
  279. *
  280. * @param bool $pValue Flag indicating whether the sheet navigation block should be generated or not
  281. *
  282. * @return HTML
  283. */
  284. public function setGenerateSheetNavigationBlock($pValue)
  285. {
  286. $this->generateSheetNavigationBlock = (bool) $pValue;
  287. return $this;
  288. }
  289. /**
  290. * Write all sheets (resets sheetIndex to NULL).
  291. */
  292. public function writeAllSheets()
  293. {
  294. $this->sheetIndex = null;
  295. return $this;
  296. }
  297. /**
  298. * Generate HTML header.
  299. *
  300. * @param bool $pIncludeStyles Include styles?
  301. *
  302. * @throws WriterException
  303. *
  304. * @return string
  305. */
  306. public function generateHTMLHeader($pIncludeStyles = false)
  307. {
  308. // Spreadsheet object known?
  309. if ($this->spreadsheet === null) {
  310. throw new WriterException('Internal Spreadsheet object not set to an instance of an object.');
  311. }
  312. // Construct HTML
  313. $properties = $this->spreadsheet->getProperties();
  314. $html = '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">' . PHP_EOL;
  315. $html .= '<!-- Generated by Spreadsheet - https://github.com/PHPOffice/Spreadsheet -->' . PHP_EOL;
  316. $html .= '<html>' . PHP_EOL;
  317. $html .= ' <head>' . PHP_EOL;
  318. $html .= ' <meta http-equiv="Content-Type" content="text/html; charset=utf-8">' . PHP_EOL;
  319. if ($properties->getTitle() > '') {
  320. $html .= ' <title>' . htmlspecialchars($properties->getTitle()) . '</title>' . PHP_EOL;
  321. }
  322. if ($properties->getCreator() > '') {
  323. $html .= ' <meta name="author" content="' . htmlspecialchars($properties->getCreator()) . '" />' . PHP_EOL;
  324. }
  325. if ($properties->getTitle() > '') {
  326. $html .= ' <meta name="title" content="' . htmlspecialchars($properties->getTitle()) . '" />' . PHP_EOL;
  327. }
  328. if ($properties->getDescription() > '') {
  329. $html .= ' <meta name="description" content="' . htmlspecialchars($properties->getDescription()) . '" />' . PHP_EOL;
  330. }
  331. if ($properties->getSubject() > '') {
  332. $html .= ' <meta name="subject" content="' . htmlspecialchars($properties->getSubject()) . '" />' . PHP_EOL;
  333. }
  334. if ($properties->getKeywords() > '') {
  335. $html .= ' <meta name="keywords" content="' . htmlspecialchars($properties->getKeywords()) . '" />' . PHP_EOL;
  336. }
  337. if ($properties->getCategory() > '') {
  338. $html .= ' <meta name="category" content="' . htmlspecialchars($properties->getCategory()) . '" />' . PHP_EOL;
  339. }
  340. if ($properties->getCompany() > '') {
  341. $html .= ' <meta name="company" content="' . htmlspecialchars($properties->getCompany()) . '" />' . PHP_EOL;
  342. }
  343. if ($properties->getManager() > '') {
  344. $html .= ' <meta name="manager" content="' . htmlspecialchars($properties->getManager()) . '" />' . PHP_EOL;
  345. }
  346. if ($pIncludeStyles) {
  347. $html .= $this->generateStyles(true);
  348. }
  349. $html .= ' </head>' . PHP_EOL;
  350. $html .= '' . PHP_EOL;
  351. $html .= ' <body>' . PHP_EOL;
  352. return $html;
  353. }
  354. /**
  355. * Generate sheet data.
  356. *
  357. * @throws WriterException
  358. *
  359. * @return string
  360. */
  361. public function generateSheetData()
  362. {
  363. // Spreadsheet object known?
  364. if ($this->spreadsheet === null) {
  365. throw new WriterException('Internal Spreadsheet object not set to an instance of an object.');
  366. }
  367. // Ensure that Spans have been calculated?
  368. if ($this->sheetIndex !== null || !$this->spansAreCalculated) {
  369. $this->calculateSpans();
  370. }
  371. // Fetch sheets
  372. $sheets = [];
  373. if ($this->sheetIndex === null) {
  374. $sheets = $this->spreadsheet->getAllSheets();
  375. } else {
  376. $sheets[] = $this->spreadsheet->getSheet($this->sheetIndex);
  377. }
  378. // Construct HTML
  379. $html = '';
  380. // Loop all sheets
  381. $sheetId = 0;
  382. foreach ($sheets as $sheet) {
  383. // Write table header
  384. $html .= $this->generateTableHeader($sheet);
  385. // Get worksheet dimension
  386. $dimension = explode(':', $sheet->calculateWorksheetDimension());
  387. $dimension[0] = Coordinate::coordinateFromString($dimension[0]);
  388. $dimension[0][0] = Coordinate::columnIndexFromString($dimension[0][0]);
  389. $dimension[1] = Coordinate::coordinateFromString($dimension[1]);
  390. $dimension[1][0] = Coordinate::columnIndexFromString($dimension[1][0]);
  391. // row min,max
  392. $rowMin = $dimension[0][1];
  393. $rowMax = $dimension[1][1];
  394. // calculate start of <tbody>, <thead>
  395. $tbodyStart = $rowMin;
  396. $theadStart = $theadEnd = 0; // default: no <thead> no </thead>
  397. if ($sheet->getPageSetup()->isRowsToRepeatAtTopSet()) {
  398. $rowsToRepeatAtTop = $sheet->getPageSetup()->getRowsToRepeatAtTop();
  399. // we can only support repeating rows that start at top row
  400. if ($rowsToRepeatAtTop[0] == 1) {
  401. $theadStart = $rowsToRepeatAtTop[0];
  402. $theadEnd = $rowsToRepeatAtTop[1];
  403. $tbodyStart = $rowsToRepeatAtTop[1] + 1;
  404. }
  405. }
  406. // Loop through cells
  407. $row = $rowMin - 1;
  408. while ($row++ < $rowMax) {
  409. // <thead> ?
  410. if ($row == $theadStart) {
  411. $html .= ' <thead>' . PHP_EOL;
  412. $cellType = 'th';
  413. }
  414. // <tbody> ?
  415. if ($row == $tbodyStart) {
  416. $html .= ' <tbody>' . PHP_EOL;
  417. $cellType = 'td';
  418. }
  419. // Write row if there are HTML table cells in it
  420. if (!isset($this->isSpannedRow[$sheet->getParent()->getIndex($sheet)][$row])) {
  421. // Start a new rowData
  422. $rowData = [];
  423. // Loop through columns
  424. $column = $dimension[0][0];
  425. while ($column <= $dimension[1][0]) {
  426. // Cell exists?
  427. if ($sheet->cellExistsByColumnAndRow($column, $row)) {
  428. $rowData[$column] = Coordinate::stringFromColumnIndex($column) . $row;
  429. } else {
  430. $rowData[$column] = '';
  431. }
  432. ++$column;
  433. }
  434. $html .= $this->generateRow($sheet, $rowData, $row - 1, $cellType);
  435. }
  436. // </thead> ?
  437. if ($row == $theadEnd) {
  438. $html .= ' </thead>' . PHP_EOL;
  439. }
  440. }
  441. $html .= $this->extendRowsForChartsAndImages($sheet, $row);
  442. // Close table body.
  443. $html .= ' </tbody>' . PHP_EOL;
  444. // Write table footer
  445. $html .= $this->generateTableFooter();
  446. // Writing PDF?
  447. if ($this->isPdf) {
  448. if ($this->sheetIndex === null && $sheetId + 1 < $this->spreadsheet->getSheetCount()) {
  449. $html .= '<div style="page-break-before:always" />';
  450. }
  451. }
  452. // Next sheet
  453. ++$sheetId;
  454. }
  455. return $html;
  456. }
  457. /**
  458. * Generate sheet tabs.
  459. *
  460. * @throws WriterException
  461. *
  462. * @return string
  463. */
  464. public function generateNavigation()
  465. {
  466. // Spreadsheet object known?
  467. if ($this->spreadsheet === null) {
  468. throw new WriterException('Internal Spreadsheet object not set to an instance of an object.');
  469. }
  470. // Fetch sheets
  471. $sheets = [];
  472. if ($this->sheetIndex === null) {
  473. $sheets = $this->spreadsheet->getAllSheets();
  474. } else {
  475. $sheets[] = $this->spreadsheet->getSheet($this->sheetIndex);
  476. }
  477. // Construct HTML
  478. $html = '';
  479. // Only if there are more than 1 sheets
  480. if (count($sheets) > 1) {
  481. // Loop all sheets
  482. $sheetId = 0;
  483. $html .= '<ul class="navigation">' . PHP_EOL;
  484. foreach ($sheets as $sheet) {
  485. $html .= ' <li class="sheet' . $sheetId . '"><a href="#sheet' . $sheetId . '">' . $sheet->getTitle() . '</a></li>' . PHP_EOL;
  486. ++$sheetId;
  487. }
  488. $html .= '</ul>' . PHP_EOL;
  489. }
  490. return $html;
  491. }
  492. private function extendRowsForChartsAndImages(Worksheet $pSheet, $row)
  493. {
  494. $rowMax = $row;
  495. $colMax = 'A';
  496. if ($this->includeCharts) {
  497. foreach ($pSheet->getChartCollection() as $chart) {
  498. if ($chart instanceof Chart) {
  499. $chartCoordinates = $chart->getTopLeftPosition();
  500. $chartTL = Coordinate::coordinateFromString($chartCoordinates['cell']);
  501. $chartCol = Coordinate::columnIndexFromString($chartTL[0]);
  502. if ($chartTL[1] > $rowMax) {
  503. $rowMax = $chartTL[1];
  504. if ($chartCol > Coordinate::columnIndexFromString($colMax)) {
  505. $colMax = $chartTL[0];
  506. }
  507. }
  508. }
  509. }
  510. }
  511. foreach ($pSheet->getDrawingCollection() as $drawing) {
  512. if ($drawing instanceof Drawing) {
  513. $imageTL = Coordinate::coordinateFromString($drawing->getCoordinates());
  514. $imageCol = Coordinate::columnIndexFromString($imageTL[0]);
  515. if ($imageTL[1] > $rowMax) {
  516. $rowMax = $imageTL[1];
  517. if ($imageCol > Coordinate::columnIndexFromString($colMax)) {
  518. $colMax = $imageTL[0];
  519. }
  520. }
  521. }
  522. }
  523. // Don't extend rows if not needed
  524. if ($row === $rowMax) {
  525. return '';
  526. }
  527. $html = '';
  528. ++$colMax;
  529. while ($row <= $rowMax) {
  530. $html .= '<tr>';
  531. for ($col = 'A'; $col != $colMax; ++$col) {
  532. $html .= '<td>';
  533. $html .= $this->writeImageInCell($pSheet, $col . $row);
  534. if ($this->includeCharts) {
  535. $html .= $this->writeChartInCell($pSheet, $col . $row);
  536. }
  537. $html .= '</td>';
  538. }
  539. ++$row;
  540. $html .= '</tr>';
  541. }
  542. return $html;
  543. }
  544. /**
  545. * Generate image tag in cell.
  546. *
  547. * @param Worksheet $pSheet \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet
  548. * @param string $coordinates Cell coordinates
  549. *
  550. * @return string
  551. */
  552. private function writeImageInCell(Worksheet $pSheet, $coordinates)
  553. {
  554. // Construct HTML
  555. $html = '';
  556. // Write images
  557. foreach ($pSheet->getDrawingCollection() as $drawing) {
  558. if ($drawing instanceof Drawing) {
  559. if ($drawing->getCoordinates() == $coordinates) {
  560. $filename = $drawing->getPath();
  561. // Strip off eventual '.'
  562. if (substr($filename, 0, 1) == '.') {
  563. $filename = substr($filename, 1);
  564. }
  565. // Prepend images root
  566. $filename = $this->getImagesRoot() . $filename;
  567. // Strip off eventual '.'
  568. if (substr($filename, 0, 1) == '.' && substr($filename, 0, 2) != './') {
  569. $filename = substr($filename, 1);
  570. }
  571. // Convert UTF8 data to PCDATA
  572. $filename = htmlspecialchars($filename);
  573. $html .= PHP_EOL;
  574. if ((!$this->embedImages) || ($this->isPdf)) {
  575. $imageData = $filename;
  576. } else {
  577. $imageDetails = getimagesize($filename);
  578. if ($fp = fopen($filename, 'rb', 0)) {
  579. $picture = fread($fp, filesize($filename));
  580. fclose($fp);
  581. // base64 encode the binary data, then break it
  582. // into chunks according to RFC 2045 semantics
  583. $base64 = chunk_split(base64_encode($picture));
  584. $imageData = 'data:' . $imageDetails['mime'] . ';base64,' . $base64;
  585. } else {
  586. $imageData = $filename;
  587. }
  588. }
  589. $html .= '<div style="position: relative;">';
  590. $html .= '<img style="position: absolute; z-index: 1; left: ' .
  591. $drawing->getOffsetX() . 'px; top: ' . $drawing->getOffsetY() . 'px; width: ' .
  592. $drawing->getWidth() . 'px; height: ' . $drawing->getHeight() . 'px;" src="' .
  593. $imageData . '" border="0" />';
  594. $html .= '</div>';
  595. }
  596. } elseif ($drawing instanceof MemoryDrawing) {
  597. if ($drawing->getCoordinates() != $coordinates) {
  598. continue;
  599. }
  600. ob_start(); // Let's start output buffering.
  601. imagepng($drawing->getImageResource()); // This will normally output the image, but because of ob_start(), it won't.
  602. $contents = ob_get_contents(); // Instead, output above is saved to $contents
  603. ob_end_clean(); // End the output buffer.
  604. $dataUri = 'data:image/jpeg;base64,' . base64_encode($contents);
  605. // Because of the nature of tables, width is more important than height.
  606. // max-width: 100% ensures that image doesnt overflow containing cell
  607. // width: X sets width of supplied image.
  608. // As a result, images bigger than cell will be contained and images smaller will not get stretched
  609. $html .= '<img src="' . $dataUri . '" style="max-width:100%;width:' . $drawing->getWidth() . 'px;" />';
  610. }
  611. }
  612. return $html;
  613. }
  614. /**
  615. * Generate chart tag in cell.
  616. *
  617. * @param Worksheet $pSheet \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet
  618. * @param string $coordinates Cell coordinates
  619. *
  620. * @return string
  621. */
  622. private function writeChartInCell(Worksheet $pSheet, $coordinates)
  623. {
  624. // Construct HTML
  625. $html = '';
  626. // Write charts
  627. foreach ($pSheet->getChartCollection() as $chart) {
  628. if ($chart instanceof Chart) {
  629. $chartCoordinates = $chart->getTopLeftPosition();
  630. if ($chartCoordinates['cell'] == $coordinates) {
  631. $chartFileName = File::sysGetTempDir() . '/' . uniqid('', true) . '.png';
  632. if (!$chart->render($chartFileName)) {
  633. return;
  634. }
  635. $html .= PHP_EOL;
  636. $imageDetails = getimagesize($chartFileName);
  637. if ($fp = fopen($chartFileName, 'rb', 0)) {
  638. $picture = fread($fp, filesize($chartFileName));
  639. fclose($fp);
  640. // base64 encode the binary data, then break it
  641. // into chunks according to RFC 2045 semantics
  642. $base64 = chunk_split(base64_encode($picture));
  643. $imageData = 'data:' . $imageDetails['mime'] . ';base64,' . $base64;
  644. $html .= '<div style="position: relative;">';
  645. $html .= '<img style="position: absolute; z-index: 1; left: ' . $chartCoordinates['xOffset'] . 'px; top: ' . $chartCoordinates['yOffset'] . 'px; width: ' . $imageDetails[0] . 'px; height: ' . $imageDetails[1] . 'px;" src="' . $imageData . '" border="0" />' . PHP_EOL;
  646. $html .= '</div>';
  647. unlink($chartFileName);
  648. }
  649. }
  650. }
  651. }
  652. // Return
  653. return $html;
  654. }
  655. /**
  656. * Generate CSS styles.
  657. *
  658. * @param bool $generateSurroundingHTML Generate surrounding HTML tags? (&lt;style&gt; and &lt;/style&gt;)
  659. *
  660. * @throws WriterException
  661. *
  662. * @return string
  663. */
  664. public function generateStyles($generateSurroundingHTML = true)
  665. {
  666. // Spreadsheet object known?
  667. if ($this->spreadsheet === null) {
  668. throw new WriterException('Internal Spreadsheet object not set to an instance of an object.');
  669. }
  670. // Build CSS
  671. $css = $this->buildCSS($generateSurroundingHTML);
  672. // Construct HTML
  673. $html = '';
  674. // Start styles
  675. if ($generateSurroundingHTML) {
  676. $html .= ' <style type="text/css">' . PHP_EOL;
  677. $html .= ' html { ' . $this->assembleCSS($css['html']) . ' }' . PHP_EOL;
  678. }
  679. // Write all other styles
  680. foreach ($css as $styleName => $styleDefinition) {
  681. if ($styleName != 'html') {
  682. $html .= ' ' . $styleName . ' { ' . $this->assembleCSS($styleDefinition) . ' }' . PHP_EOL;
  683. }
  684. }
  685. // End styles
  686. if ($generateSurroundingHTML) {
  687. $html .= ' </style>' . PHP_EOL;
  688. }
  689. // Return
  690. return $html;
  691. }
  692. /**
  693. * Build CSS styles.
  694. *
  695. * @param bool $generateSurroundingHTML Generate surrounding HTML style? (html { })
  696. *
  697. * @throws WriterException
  698. *
  699. * @return array
  700. */
  701. public function buildCSS($generateSurroundingHTML = true)
  702. {
  703. // Spreadsheet object known?
  704. if ($this->spreadsheet === null) {
  705. throw new WriterException('Internal Spreadsheet object not set to an instance of an object.');
  706. }
  707. // Cached?
  708. if ($this->cssStyles !== null) {
  709. return $this->cssStyles;
  710. }
  711. // Ensure that spans have been calculated
  712. if (!$this->spansAreCalculated) {
  713. $this->calculateSpans();
  714. }
  715. // Construct CSS
  716. $css = [];
  717. // Start styles
  718. if ($generateSurroundingHTML) {
  719. // html { }
  720. $css['html']['font-family'] = 'Calibri, Arial, Helvetica, sans-serif';
  721. $css['html']['font-size'] = '11pt';
  722. $css['html']['background-color'] = 'white';
  723. }
  724. // CSS for comments as found in LibreOffice
  725. $css['a.comment-indicator:hover + div.comment'] = [
  726. 'background' => '#ffd',
  727. 'position' => 'absolute',
  728. 'display' => 'block',
  729. 'border' => '1px solid black',
  730. 'padding' => '0.5em',
  731. ];
  732. $css['a.comment-indicator'] = [
  733. 'background' => 'red',
  734. 'display' => 'inline-block',
  735. 'border' => '1px solid black',
  736. 'width' => '0.5em',
  737. 'height' => '0.5em',
  738. ];
  739. $css['div.comment']['display'] = 'none';
  740. // table { }
  741. $css['table']['border-collapse'] = 'collapse';
  742. if (!$this->isPdf) {
  743. $css['table']['page-break-after'] = 'always';
  744. }
  745. // .gridlines td { }
  746. $css['.gridlines td']['border'] = '1px dotted black';
  747. $css['.gridlines th']['border'] = '1px dotted black';
  748. // .b {}
  749. $css['.b']['text-align'] = 'center'; // BOOL
  750. // .e {}
  751. $css['.e']['text-align'] = 'center'; // ERROR
  752. // .f {}
  753. $css['.f']['text-align'] = 'right'; // FORMULA
  754. // .inlineStr {}
  755. $css['.inlineStr']['text-align'] = 'left'; // INLINE
  756. // .n {}
  757. $css['.n']['text-align'] = 'right'; // NUMERIC
  758. // .s {}
  759. $css['.s']['text-align'] = 'left'; // STRING
  760. // Calculate cell style hashes
  761. foreach ($this->spreadsheet->getCellXfCollection() as $index => $style) {
  762. $css['td.style' . $index] = $this->createCSSStyle($style);
  763. $css['th.style' . $index] = $this->createCSSStyle($style);
  764. }
  765. // Fetch sheets
  766. $sheets = [];
  767. if ($this->sheetIndex === null) {
  768. $sheets = $this->spreadsheet->getAllSheets();
  769. } else {
  770. $sheets[] = $this->spreadsheet->getSheet($this->sheetIndex);
  771. }
  772. // Build styles per sheet
  773. foreach ($sheets as $sheet) {
  774. // Calculate hash code
  775. $sheetIndex = $sheet->getParent()->getIndex($sheet);
  776. // Build styles
  777. // Calculate column widths
  778. $sheet->calculateColumnWidths();
  779. // col elements, initialize
  780. $highestColumnIndex = Coordinate::columnIndexFromString($sheet->getHighestColumn()) - 1;
  781. $column = -1;
  782. while ($column++ < $highestColumnIndex) {
  783. $this->columnWidths[$sheetIndex][$column] = 42; // approximation
  784. $css['table.sheet' . $sheetIndex . ' col.col' . $column]['width'] = '42pt';
  785. }
  786. // col elements, loop through columnDimensions and set width
  787. foreach ($sheet->getColumnDimensions() as $columnDimension) {
  788. if (($width = SharedDrawing::cellDimensionToPixels($columnDimension->getWidth(), $this->defaultFont)) >= 0) {
  789. $width = SharedDrawing::pixelsToPoints($width);
  790. $column = Coordinate::columnIndexFromString($columnDimension->getColumnIndex()) - 1;
  791. $this->columnWidths[$sheetIndex][$column] = $width;
  792. $css['table.sheet' . $sheetIndex . ' col.col' . $column]['width'] = $width . 'pt';
  793. if ($columnDimension->getVisible() === false) {
  794. $css['table.sheet' . $sheetIndex . ' col.col' . $column]['visibility'] = 'collapse';
  795. $css['table.sheet' . $sheetIndex . ' col.col' . $column]['*display'] = 'none'; // target IE6+7
  796. }
  797. }
  798. }
  799. // Default row height
  800. $rowDimension = $sheet->getDefaultRowDimension();
  801. // table.sheetN tr { }
  802. $css['table.sheet' . $sheetIndex . ' tr'] = [];
  803. if ($rowDimension->getRowHeight() == -1) {
  804. $pt_height = SharedFont::getDefaultRowHeightByFont($this->spreadsheet->getDefaultStyle()->getFont());
  805. } else {
  806. $pt_height = $rowDimension->getRowHeight();
  807. }
  808. $css['table.sheet' . $sheetIndex . ' tr']['height'] = $pt_height . 'pt';
  809. if ($rowDimension->getVisible() === false) {
  810. $css['table.sheet' . $sheetIndex . ' tr']['display'] = 'none';
  811. $css['table.sheet' . $sheetIndex . ' tr']['visibility'] = 'hidden';
  812. }
  813. // Calculate row heights
  814. foreach ($sheet->getRowDimensions() as $rowDimension) {
  815. $row = $rowDimension->getRowIndex() - 1;
  816. // table.sheetN tr.rowYYYYYY { }
  817. $css['table.sheet' . $sheetIndex . ' tr.row' . $row] = [];
  818. if ($rowDimension->getRowHeight() == -1) {
  819. $pt_height = SharedFont::getDefaultRowHeightByFont($this->spreadsheet->getDefaultStyle()->getFont());
  820. } else {
  821. $pt_height = $rowDimension->getRowHeight();
  822. }
  823. $css['table.sheet' . $sheetIndex . ' tr.row' . $row]['height'] = $pt_height . 'pt';
  824. if ($rowDimension->getVisible() === false) {
  825. $css['table.sheet' . $sheetIndex . ' tr.row' . $row]['display'] = 'none';
  826. $css['table.sheet' . $sheetIndex . ' tr.row' . $row]['visibility'] = 'hidden';
  827. }
  828. }
  829. }
  830. // Cache
  831. if ($this->cssStyles === null) {
  832. $this->cssStyles = $css;
  833. }
  834. // Return
  835. return $css;
  836. }
  837. /**
  838. * Create CSS style.
  839. *
  840. * @param Style $pStyle
  841. *
  842. * @return array
  843. */
  844. private function createCSSStyle(Style $pStyle)
  845. {
  846. // Create CSS
  847. $css = array_merge(
  848. $this->createCSSStyleAlignment($pStyle->getAlignment()),
  849. $this->createCSSStyleBorders($pStyle->getBorders()),
  850. $this->createCSSStyleFont($pStyle->getFont()),
  851. $this->createCSSStyleFill($pStyle->getFill())
  852. );
  853. // Return
  854. return $css;
  855. }
  856. /**
  857. * Create CSS style (\PhpOffice\PhpSpreadsheet\Style\Alignment).
  858. *
  859. * @param Alignment $pStyle \PhpOffice\PhpSpreadsheet\Style\Alignment
  860. *
  861. * @return array
  862. */
  863. private function createCSSStyleAlignment(Alignment $pStyle)
  864. {
  865. // Construct CSS
  866. $css = [];
  867. // Create CSS
  868. $css['vertical-align'] = $this->mapVAlign($pStyle->getVertical());
  869. if ($textAlign = $this->mapHAlign($pStyle->getHorizontal())) {
  870. $css['text-align'] = $textAlign;
  871. if (in_array($textAlign, ['left', 'right'])) {
  872. $css['padding-' . $textAlign] = (string) ((int) $pStyle->getIndent() * 9) . 'px';
  873. }
  874. }
  875. return $css;
  876. }
  877. /**
  878. * Create CSS style (\PhpOffice\PhpSpreadsheet\Style\Font).
  879. *
  880. * @param Font $pStyle
  881. *
  882. * @return array
  883. */
  884. private function createCSSStyleFont(Font $pStyle)
  885. {
  886. // Construct CSS
  887. $css = [];
  888. // Create CSS
  889. if ($pStyle->getBold()) {
  890. $css['font-weight'] = 'bold';
  891. }
  892. if ($pStyle->getUnderline() != Font::UNDERLINE_NONE && $pStyle->getStrikethrough()) {
  893. $css['text-decoration'] = 'underline line-through';
  894. } elseif ($pStyle->getUnderline() != Font::UNDERLINE_NONE) {
  895. $css['text-decoration'] = 'underline';
  896. } elseif ($pStyle->getStrikethrough()) {
  897. $css['text-decoration'] = 'line-through';
  898. }
  899. if ($pStyle->getItalic()) {
  900. $css['font-style'] = 'italic';
  901. }
  902. $css['color'] = '#' . $pStyle->getColor()->getRGB();
  903. $css['font-family'] = '\'' . $pStyle->getName() . '\'';
  904. $css['font-size'] = $pStyle->getSize() . 'pt';
  905. return $css;
  906. }
  907. /**
  908. * Create CSS style (Borders).
  909. *
  910. * @param Borders $pStyle Borders
  911. *
  912. * @return array
  913. */
  914. private function createCSSStyleBorders(Borders $pStyle)
  915. {
  916. // Construct CSS
  917. $css = [];
  918. // Create CSS
  919. $css['border-bottom'] = $this->createCSSStyleBorder($pStyle->getBottom());
  920. $css['border-top'] = $this->createCSSStyleBorder($pStyle->getTop());
  921. $css['border-left'] = $this->createCSSStyleBorder($pStyle->getLeft());
  922. $css['border-right'] = $this->createCSSStyleBorder($pStyle->getRight());
  923. return $css;
  924. }
  925. /**
  926. * Create CSS style (Border).
  927. *
  928. * @param Border $pStyle Border
  929. *
  930. * @return string
  931. */
  932. private function createCSSStyleBorder(Border $pStyle)
  933. {
  934. // Create CSS - add !important to non-none border styles for merged cells
  935. $borderStyle = $this->mapBorderStyle($pStyle->getBorderStyle());
  936. $css = $borderStyle . ' #' . $pStyle->getColor()->getRGB() . (($borderStyle == 'none') ? '' : ' !important');
  937. return $css;
  938. }
  939. /**
  940. * Create CSS style (Fill).
  941. *
  942. * @param Fill $pStyle Fill
  943. *
  944. * @return array
  945. */
  946. private function createCSSStyleFill(Fill $pStyle)
  947. {
  948. // Construct HTML
  949. $css = [];
  950. // Create CSS
  951. $value = $pStyle->getFillType() == Fill::FILL_NONE ?
  952. 'white' : '#' . $pStyle->getStartColor()->getRGB();
  953. $css['background-color'] = $value;
  954. return $css;
  955. }
  956. /**
  957. * Generate HTML footer.
  958. */
  959. public function generateHTMLFooter()
  960. {
  961. // Construct HTML
  962. $html = '';
  963. $html .= ' </body>' . PHP_EOL;
  964. $html .= '</html>' . PHP_EOL;
  965. return $html;
  966. }
  967. /**
  968. * Generate table header.
  969. *
  970. * @param Worksheet $pSheet The worksheet for the table we are writing
  971. *
  972. * @return string
  973. */
  974. private function generateTableHeader($pSheet)
  975. {
  976. $sheetIndex = $pSheet->getParent()->getIndex($pSheet);
  977. // Construct HTML
  978. $html = '';
  979. $html .= $this->setMargins($pSheet);
  980. if (!$this->useInlineCss) {
  981. $gridlines = $pSheet->getShowGridlines() ? ' gridlines' : '';
  982. $html .= ' <table border="0" cellpadding="0" cellspacing="0" id="sheet' . $sheetIndex . '" class="sheet' . $sheetIndex . $gridlines . '">' . PHP_EOL;
  983. } else {
  984. $style = isset($this->cssStyles['table']) ?
  985. $this->assembleCSS($this->cssStyles['table']) : '';
  986. if ($this->isPdf && $pSheet->getShowGridlines()) {
  987. $html .= ' <table border="1" cellpadding="1" id="sheet' . $sheetIndex . '" cellspacing="1" style="' . $style . '">' . PHP_EOL;
  988. } else {
  989. $html .= ' <table border="0" cellpadding="1" id="sheet' . $sheetIndex . '" cellspacing="0" style="' . $style . '">' . PHP_EOL;
  990. }
  991. }
  992. // Write <col> elements
  993. $highestColumnIndex = Coordinate::columnIndexFromString($pSheet->getHighestColumn()) - 1;
  994. $i = -1;
  995. while ($i++ < $highestColumnIndex) {
  996. if (!$this->isPdf) {
  997. if (!$this->useInlineCss) {
  998. $html .= ' <col class="col' . $i . '">' . PHP_EOL;
  999. } else {
  1000. $style = isset($this->cssStyles['table.sheet' . $sheetIndex . ' col.col' . $i]) ?
  1001. $this->assembleCSS($this->cssStyles['table.sheet' . $sheetIndex . ' col.col' . $i]) : '';
  1002. $html .= ' <col style="' . $style . '">' . PHP_EOL;
  1003. }
  1004. }
  1005. }
  1006. return $html;
  1007. }
  1008. /**
  1009. * Generate table footer.
  1010. */
  1011. private function generateTableFooter()
  1012. {
  1013. $html = ' </table>' . PHP_EOL;
  1014. return $html;
  1015. }
  1016. /**
  1017. * Generate row.
  1018. *
  1019. * @param Worksheet $pSheet \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet
  1020. * @param array $pValues Array containing cells in a row
  1021. * @param int $pRow Row number (0-based)
  1022. * @param string $cellType eg: 'td'
  1023. *
  1024. * @throws WriterException
  1025. *
  1026. * @return string
  1027. */
  1028. private function generateRow(Worksheet $pSheet, array $pValues, $pRow, $cellType)
  1029. {
  1030. // Construct HTML
  1031. $html = '';
  1032. // Sheet index
  1033. $sheetIndex = $pSheet->getParent()->getIndex($pSheet);
  1034. // Dompdf and breaks
  1035. if ($this->isPdf && count($pSheet->getBreaks()) > 0) {
  1036. $breaks = $pSheet->getBreaks();
  1037. // check if a break is needed before this row
  1038. if (isset($breaks['A' . $pRow])) {
  1039. // close table: </table>
  1040. $html .= $this->generateTableFooter();
  1041. // insert page break
  1042. $html .= '<div style="page-break-before:always" />';
  1043. // open table again: <table> + <col> etc.
  1044. $html .= $this->generateTableHeader($pSheet);
  1045. }
  1046. }
  1047. // Write row start
  1048. if (!$this->useInlineCss) {
  1049. $html .= ' <tr class="row' . $pRow . '">' . PHP_EOL;
  1050. } else {
  1051. $style = isset($this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $pRow])
  1052. ? $this->assembleCSS($this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $pRow]) : '';
  1053. $html .= ' <tr style="' . $style . '">' . PHP_EOL;
  1054. }
  1055. // Write cells
  1056. $colNum = 0;
  1057. foreach ($pValues as $cellAddress) {
  1058. $cell = ($cellAddress > '') ? $pSheet->getCell($cellAddress) : '';
  1059. $coordinate = Coordinate::stringFromColumnIndex($colNum + 1) . ($pRow + 1);
  1060. if (!$this->useInlineCss) {
  1061. $cssClass = 'column' . $colNum;
  1062. } else {
  1063. $cssClass = [];
  1064. if ($cellType == 'th') {
  1065. if (isset($this->cssStyles['table.sheet' . $sheetIndex . ' th.column' . $colNum])) {
  1066. $this->cssStyles['table.sheet' . $sheetIndex . ' th.column' . $colNum];
  1067. }
  1068. } else {
  1069. if (isset($this->cssStyles['table.sheet' . $sheetIndex . ' td.column' . $colNum])) {
  1070. $this->cssStyles['table.sheet' . $sheetIndex . ' td.column' . $colNum];
  1071. }
  1072. }
  1073. }
  1074. $colSpan = 1;
  1075. $rowSpan = 1;
  1076. // initialize
  1077. $cellData = '&nbsp;';
  1078. // Cell
  1079. if ($cell instanceof Cell) {
  1080. $cellData = '';
  1081. if ($cell->getParent() === null) {
  1082. $cell->attach($pSheet);
  1083. }
  1084. // Value
  1085. if ($cell->getValue() instanceof RichText) {
  1086. // Loop through rich text elements
  1087. $elements = $cell->getValue()->getRichTextElements();
  1088. foreach ($elements as $element) {
  1089. // Rich text start?
  1090. if ($element instanceof Run) {
  1091. $cellData .= '<span style="' . $this->assembleCSS($this->createCSSStyleFont($element->getFont())) . '">';
  1092. if ($element->getFont()->getSuperscript()) {
  1093. $cellData .= '<sup>';
  1094. } elseif ($element->getFont()->getSubscript()) {
  1095. $cellData .= '<sub>';
  1096. }
  1097. }
  1098. // Convert UTF8 data to PCDATA
  1099. $cellText = $element->getText();
  1100. $cellData .= htmlspecialchars($cellText);
  1101. if ($element instanceof Run) {
  1102. if ($element->getFont()->getSuperscript()) {
  1103. $cellData .= '</sup>';
  1104. } elseif ($element->getFont()->getSubscript()) {
  1105. $cellData .= '</sub>';
  1106. }
  1107. $cellData .= '</span>';
  1108. }
  1109. }
  1110. } else {
  1111. if ($this->preCalculateFormulas) {
  1112. $cellData = NumberFormat::toFormattedString(
  1113. $cell->getCalculatedValue(),
  1114. $pSheet->getParent()->getCellXfByIndex($cell->getXfIndex())->getNumberFormat()->getFormatCode(),
  1115. [$this, 'formatColor']
  1116. );
  1117. } else {
  1118. $cellData = NumberFormat::toFormattedString(
  1119. $cell->getValue(),
  1120. $pSheet->getParent()->getCellXfByIndex($cell->getXfIndex())->getNumberFormat()->getFormatCode(),
  1121. [$this, 'formatColor']
  1122. );
  1123. }
  1124. $cellData = htmlspecialchars($cellData);
  1125. if ($pSheet->getParent()->getCellXfByIndex($cell->getXfIndex())->getFont()->getSuperscript()) {
  1126. $cellData = '<sup>' . $cellData . '</sup>';
  1127. } elseif ($pSheet->getParent()->getCellXfByIndex($cell->getXfIndex())->getFont()->getSubscript()) {
  1128. $cellData = '<sub>' . $cellData . '</sub>';
  1129. }
  1130. }
  1131. // Converts the cell content so that spaces occuring at beginning of each new line are replaced by &nbsp;
  1132. // Example: " Hello\n to the world" is converted to "&nbsp;&nbsp;Hello\n&nbsp;to the world"
  1133. $cellData = preg_replace('/(?m)(?:^|\\G) /', '&nbsp;', $cellData);
  1134. // convert newline "\n" to '<br>'
  1135. $cellData = nl2br($cellData);
  1136. // Extend CSS class?
  1137. if (!$this->useInlineCss) {
  1138. $cssClass .= ' style' . $cell->getXfIndex();
  1139. $cssClass .= ' ' . $cell->getDataType();
  1140. } else {
  1141. if ($cellType == 'th') {
  1142. if (isset($this->cssStyles['th.style' . $cell->getXfIndex()])) {
  1143. $cssClass = array_merge($cssClass, $this->cssStyles['th.style' . $cell->getXfIndex()]);
  1144. }
  1145. } else {
  1146. if (isset($this->cssStyles['td.style' . $cell->getXfIndex()])) {
  1147. $cssClass = array_merge($cssClass, $this->cssStyles['td.style' . $cell->getXfIndex()]);
  1148. }
  1149. }
  1150. // General horizontal alignment: Actual horizontal alignment depends on dataType
  1151. $sharedStyle = $pSheet->getParent()->getCellXfByIndex($cell->getXfIndex());
  1152. if ($sharedStyle->getAlignment()->getHorizontal() == Alignment::HORIZONTAL_GENERAL
  1153. && isset($this->cssStyles['.' . $cell->getDataType()]['text-align'])
  1154. ) {
  1155. $cssClass['text-align'] = $this->cssStyles['.' . $cell->getDataType()]['text-align'];
  1156. }
  1157. }
  1158. }
  1159. // Hyperlink?
  1160. if ($pSheet->hyperlinkExists($coordinate) && !$pSheet->getHyperlink($coordinate)->isInternal()) {
  1161. $cellData = '<a href="' . htmlspecialchars($pSheet->getHyperlink($coordinate)->getUrl()) . '" title="' . htmlspecialchars($pSheet->getHyperlink($coordinate)->getTooltip()) . '">' . $cellData . '</a>';
  1162. }
  1163. // Should the cell be written or is it swallowed by a rowspan or colspan?
  1164. $writeCell = !(isset($this->isSpannedCell[$pSheet->getParent()->getIndex($pSheet)][$pRow + 1][$colNum])
  1165. && $this->isSpannedCell[$pSheet->getParent()->getIndex($pSheet)][$pRow + 1][$colNum]);
  1166. // Colspan and Rowspan
  1167. $colspan = 1;
  1168. $rowspan = 1;
  1169. if (isset($this->isBaseCell[$pSheet->getParent()->getIndex($pSheet)][$pRow + 1][$colNum])) {
  1170. $spans = $this->isBaseCell[$pSheet->getParent()->getIndex($pSheet)][$pRow + 1][$colNum];
  1171. $rowSpan = $spans['rowspan'];
  1172. $colSpan = $spans['colspan'];
  1173. // Also apply style from last cell in merge to fix borders -
  1174. // relies on !important for non-none border declarations in createCSSStyleBorder
  1175. $endCellCoord = Coordinate::stringFromColumnIndex($colNum + $colSpan) . ($pRow + $rowSpan);
  1176. if (!$this->useInlineCss) {
  1177. $cssClass .= ' style' . $pSheet->getCell($endCellCoord)->getXfIndex();
  1178. }
  1179. }
  1180. // Write
  1181. if ($writeCell) {
  1182. // Column start
  1183. $html .= ' <' . $cellType;
  1184. if (!$this->useInlineCss) {
  1185. $html .= ' class="' . $cssClass . '"';
  1186. } else {
  1187. //** Necessary redundant code for the sake of \PhpOffice\PhpSpreadsheet\Writer\Pdf **
  1188. // We must explicitly write the width of the <td> element because TCPDF
  1189. // does not recognize e.g. <col style="width:42pt">
  1190. $width = 0;
  1191. $i = $colNum - 1;
  1192. $e = $colNum + $colSpan - 1;
  1193. while ($i++ < $e) {
  1194. if (isset($this->columnWidths[$sheetIndex][$i])) {
  1195. $width += $this->columnWidths[$sheetIndex][$i];
  1196. }
  1197. }
  1198. $cssClass['width'] = $width . 'pt';
  1199. // We must also explicitly write the height of the <td> element because TCPDF
  1200. // does not recognize e.g. <tr style="height:50pt">
  1201. if (isset($this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $pRow]['height'])) {
  1202. $height = $this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $pRow]['height'];
  1203. $cssClass['height'] = $height;
  1204. }
  1205. //** end of redundant code **
  1206. $html .= ' style="' . $this->assembleCSS($cssClass) . '"';
  1207. }
  1208. if ($colSpan > 1) {
  1209. $html .= ' colspan="' . $colSpan . '"';
  1210. }
  1211. if ($rowSpan > 1) {
  1212. $html .= ' rowspan="' . $rowSpan . '"';
  1213. }
  1214. $html .= '>';
  1215. $html .= $this->writeComment($pSheet, $coordinate);
  1216. // Image?
  1217. $html .= $this->writeImageInCell($pSheet, $coordinate);
  1218. // Chart?
  1219. if ($this->includeCharts) {
  1220. $html .= $this->writeChartInCell($pSheet, $coordinate);
  1221. }
  1222. // Cell data
  1223. $html .= $cellData;
  1224. // Column end
  1225. $html .= '</' . $cellType . '>' . PHP_EOL;
  1226. }
  1227. // Next column
  1228. ++$colNum;
  1229. }
  1230. // Write row end
  1231. $html .= ' </tr>' . PHP_EOL;
  1232. // Return
  1233. return $html;
  1234. }
  1235. /**
  1236. * Takes array where of CSS properties / values and converts to CSS string.
  1237. *
  1238. * @param array $pValue
  1239. *
  1240. * @return string
  1241. */
  1242. private function assembleCSS(array $pValue = [])
  1243. {
  1244. $pairs = [];
  1245. foreach ($pValue as $property => $value) {
  1246. $pairs[] = $property . ':' . $value;
  1247. }
  1248. $string = implode('; ', $pairs);
  1249. return $string;
  1250. }
  1251. /**
  1252. * Get images root.
  1253. *
  1254. * @return string
  1255. */
  1256. public function getImagesRoot()
  1257. {
  1258. return $this->imagesRoot;
  1259. }
  1260. /**
  1261. * Set images root.
  1262. *
  1263. * @param string $pValue
  1264. *
  1265. * @return HTML
  1266. */
  1267. public function setImagesRoot($pValue)
  1268. {
  1269. $this->imagesRoot = $pValue;
  1270. return $this;
  1271. }
  1272. /**
  1273. * Get embed images.
  1274. *
  1275. * @return bool
  1276. */
  1277. public function getEmbedImages()
  1278. {
  1279. return $this->embedImages;
  1280. }
  1281. /**
  1282. * Set embed images.
  1283. *
  1284. * @param bool $pValue
  1285. *
  1286. * @return HTML
  1287. */
  1288. public function setEmbedImages($pValue)
  1289. {
  1290. $this->embedImages = $pValue;
  1291. return $this;
  1292. }
  1293. /**
  1294. * Get use inline CSS?
  1295. *
  1296. * @return bool
  1297. */
  1298. public function getUseInlineCss()
  1299. {
  1300. return $this->useInlineCss;
  1301. }
  1302. /**
  1303. * Set use inline CSS?
  1304. *
  1305. * @param bool $pValue
  1306. *
  1307. * @return HTML
  1308. */
  1309. public function setUseInlineCss($pValue)
  1310. {
  1311. $this->useInlineCss = $pValue;
  1312. return $this;
  1313. }
  1314. /**
  1315. * Add color to formatted string as inline style.
  1316. *
  1317. * @param string $pValue Plain formatted value without color
  1318. * @param string $pFormat Format code
  1319. *
  1320. * @return string
  1321. */
  1322. public function formatColor($pValue, $pFormat)
  1323. {
  1324. // Color information, e.g. [Red] is always at the beginning
  1325. $color = null; // initialize
  1326. $matches = [];
  1327. $color_regex = '/^\\[[a-zA-Z]+\\]/';
  1328. if (preg_match($color_regex, $pFormat, $matches)) {
  1329. $color = str_replace(['[', ']'], '', $matches[0]);
  1330. $color = strtolower($color);
  1331. }
  1332. // convert to PCDATA
  1333. $value = htmlspecialchars($pValue);
  1334. // color span tag
  1335. if ($color !== null) {
  1336. $value = '<span style="color:' . $color . '">' . $value . '</span>';
  1337. }
  1338. return $value;
  1339. }
  1340. /**
  1341. * Calculate information about HTML colspan and rowspan which is not always the same as Excel's.
  1342. */
  1343. private function calculateSpans()
  1344. {
  1345. // Identify all cells that should be omitted in HTML due to cell merge.
  1346. // In HTML only the upper-left cell should be written and it should have
  1347. // appropriate rowspan / colspan attribute
  1348. $sheetIndexes = $this->sheetIndex !== null ?
  1349. [$this->sheetIndex] : range(0, $this->spreadsheet->getSheetCount() - 1);
  1350. foreach ($sheetIndexes as $sheetIndex) {
  1351. $sheet = $this->spreadsheet->getSheet($sheetIndex);
  1352. $candidateSpannedRow = [];
  1353. // loop through all Excel merged cells
  1354. foreach ($sheet->getMergeCells() as $cells) {
  1355. list($cells) = Coordinate::splitRange($cells);
  1356. $first = $cells[0];
  1357. $last = $cells[1];
  1358. list($fc, $fr) = Coordinate::coordinateFromString($first);
  1359. $fc = Coordinate::columnIndexFromString($fc) - 1;
  1360. list($lc, $lr) = Coordinate::coordinateFromString($last);
  1361. $lc = Coordinate::columnIndexFromString($lc) - 1;
  1362. // loop through the individual cells in the individual merge
  1363. $r = $fr - 1;
  1364. while ($r++ < $lr) {
  1365. // also, flag this row as a HTML row that is candidate to be omitted
  1366. $candidateSpannedRow[$r] = $r;
  1367. $c = $fc - 1;
  1368. while ($c++ < $lc) {
  1369. if (!($c == $fc && $r == $fr)) {
  1370. // not the upper-left cell (should not be written in HTML)
  1371. $this->isSpannedCell[$sheetIndex][$r][$c] = [
  1372. 'baseCell' => [$fr, $fc],
  1373. ];
  1374. } else {
  1375. // upper-left is the base cell that should hold the colspan/rowspan attribute
  1376. $this->isBaseCell[$sheetIndex][$r][$c] = [
  1377. 'xlrowspan' => $lr - $fr + 1, // Excel rowspan
  1378. 'rowspan' => $lr - $fr + 1, // HTML rowspan, value may change
  1379. 'xlcolspan' => $lc - $fc + 1, // Excel colspan
  1380. 'colspan' => $lc - $fc + 1, // HTML colspan, value may change
  1381. ];
  1382. }
  1383. }
  1384. }
  1385. }
  1386. // Identify which rows should be omitted in HTML. These are the rows where all the cells
  1387. // participate in a merge and the where base cells are somewhere above.
  1388. $countColumns = Coordinate::columnIndexFromString($sheet->getHighestColumn());
  1389. foreach ($candidateSpannedRow as $rowIndex) {
  1390. if (isset($this->isSpannedCell[$sheetIndex][$rowIndex])) {
  1391. if (count($this->isSpannedCell[$sheetIndex][$rowIndex]) == $countColumns) {
  1392. $this->isSpannedRow[$sheetIndex][$rowIndex] = $rowIndex;
  1393. }
  1394. }
  1395. }
  1396. // For each of the omitted rows we found above, the affected rowspans should be subtracted by 1
  1397. if (isset($this->isSpannedRow[$sheetIndex])) {
  1398. foreach ($this->isSpannedRow[$sheetIndex] as $rowIndex) {
  1399. $adjustedBaseCells = [];
  1400. $c = -1;
  1401. $e = $countColumns - 1;
  1402. while ($c++ < $e) {
  1403. $baseCell = $this->isSpannedCell[$sheetIndex][$rowIndex][$c]['baseCell'];
  1404. if (!in_array($baseCell, $adjustedBaseCells)) {
  1405. // subtract rowspan by 1
  1406. --$this->isBaseCell[$sheetIndex][$baseCell[0]][$baseCell[1]]['rowspan'];
  1407. $adjustedBaseCells[] = $baseCell;
  1408. }
  1409. }
  1410. }
  1411. }
  1412. // TODO: Same for columns
  1413. }
  1414. // We have calculated the spans
  1415. $this->spansAreCalculated = true;
  1416. }
  1417. private function setMargins(Worksheet $pSheet)
  1418. {
  1419. $htmlPage = '@page { ';
  1420. $htmlBody = 'body { ';
  1421. $left = StringHelper::formatNumber($pSheet->getPageMargins()->getLeft()) . 'in; ';
  1422. $htmlPage .= 'margin-left: ' . $left;
  1423. $htmlBody .= 'margin-left: ' . $left;
  1424. $right = StringHelper::formatNumber($pSheet->getPageMargins()->getRight()) . 'in; ';
  1425. $htmlPage .= 'margin-right: ' . $right;
  1426. $htmlBody .= 'margin-right: ' . $right;
  1427. $top = StringHelper::formatNumber($pSheet->getPageMargins()->getTop()) . 'in; ';
  1428. $htmlPage .= 'margin-top: ' . $top;
  1429. $htmlBody .= 'margin-top: ' . $top;
  1430. $bottom = StringHelper::formatNumber($pSheet->getPageMargins()->getBottom()) . 'in; ';
  1431. $htmlPage .= 'margin-bottom: ' . $bottom;
  1432. $htmlBody .= 'margin-bottom: ' . $bottom;
  1433. $htmlPage .= "}\n";
  1434. $htmlBody .= "}\n";
  1435. return "<style>\n" . $htmlPage . $htmlBody . "</style>\n";
  1436. }
  1437. /**
  1438. * Write a comment in the same format as LibreOffice.
  1439. *
  1440. * @see https://github.com/LibreOffice/core/blob/9fc9bf3240f8c62ad7859947ab8a033ac1fe93fa/sc/source/filter/html/htmlexp.cxx#L1073-L1092
  1441. *
  1442. * @param Worksheet $pSheet
  1443. * @param string $coordinate
  1444. *
  1445. * @return string
  1446. */
  1447. private function writeComment(Worksheet $pSheet, $coordinate)
  1448. {
  1449. $result = '';
  1450. if (!$this->isPdf && isset($pSheet->getComments()[$coordinate])) {
  1451. $result .= '<a class="comment-indicator"></a>';
  1452. $result .= '<div class="comment">' . nl2br($pSheet->getComment($coordinate)->getText()->getPlainText()) . '</div>';
  1453. $result .= PHP_EOL;
  1454. }
  1455. return $result;
  1456. }
  1457. }