Html.php 56 KB

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