Style.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649
  1. <?php
  2. namespace PhpOffice\PhpSpreadsheet\Style;
  3. require_once DOL_DOCUMENT_ROOT . '/includes/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Supervisor.php';
  4. require_once DOL_DOCUMENT_ROOT . '/includes/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Font.php';
  5. require_once DOL_DOCUMENT_ROOT . '/includes/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Fill.php';
  6. require_once DOL_DOCUMENT_ROOT . '/includes/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Borders.php';
  7. require_once DOL_DOCUMENT_ROOT . '/includes/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Alignment.php';
  8. require_once DOL_DOCUMENT_ROOT . '/includes/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat.php';
  9. require_once DOL_DOCUMENT_ROOT . '/includes/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Protection.php';
  10. use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
  11. use PhpOffice\PhpSpreadsheet\Spreadsheet;
  12. class Style extends Supervisor
  13. {
  14. /**
  15. * Font.
  16. *
  17. * @var Font
  18. */
  19. protected $font;
  20. /**
  21. * Fill.
  22. *
  23. * @var Fill
  24. */
  25. protected $fill;
  26. /**
  27. * Borders.
  28. *
  29. * @var Borders
  30. */
  31. protected $borders;
  32. /**
  33. * Alignment.
  34. *
  35. * @var Alignment
  36. */
  37. protected $alignment;
  38. /**
  39. * Number Format.
  40. *
  41. * @var NumberFormat
  42. */
  43. protected $numberFormat;
  44. /**
  45. * Conditional styles.
  46. *
  47. * @var Conditional[]
  48. */
  49. protected $conditionalStyles;
  50. /**
  51. * Protection.
  52. *
  53. * @var Protection
  54. */
  55. protected $protection;
  56. /**
  57. * Index of style in collection. Only used for real style.
  58. *
  59. * @var int
  60. */
  61. protected $index;
  62. /**
  63. * Use Quote Prefix when displaying in cell editor. Only used for real style.
  64. *
  65. * @var bool
  66. */
  67. protected $quotePrefix = false;
  68. /**
  69. * Create a new Style.
  70. *
  71. * @param bool $isSupervisor Flag indicating if this is a supervisor or not
  72. * Leave this value at default unless you understand exactly what
  73. * its ramifications are
  74. * @param bool $isConditional Flag indicating if this is a conditional style or not
  75. * Leave this value at default unless you understand exactly what
  76. * its ramifications are
  77. */
  78. public function __construct($isSupervisor = false, $isConditional = false)
  79. {
  80. parent::__construct($isSupervisor);
  81. // Initialise values
  82. $this->conditionalStyles = [];
  83. $this->font = new Font($isSupervisor, $isConditional);
  84. $this->fill = new Fill($isSupervisor, $isConditional);
  85. $this->borders = new Borders($isSupervisor, $isConditional);
  86. $this->alignment = new Alignment($isSupervisor, $isConditional);
  87. $this->numberFormat = new NumberFormat($isSupervisor, $isConditional);
  88. $this->protection = new Protection($isSupervisor, $isConditional);
  89. // bind parent if we are a supervisor
  90. if ($isSupervisor) {
  91. $this->font->bindParent($this);
  92. $this->fill->bindParent($this);
  93. $this->borders->bindParent($this);
  94. $this->alignment->bindParent($this);
  95. $this->numberFormat->bindParent($this);
  96. $this->protection->bindParent($this);
  97. }
  98. }
  99. /**
  100. * Get the shared style component for the currently active cell in currently active sheet.
  101. * Only used for style supervisor.
  102. *
  103. * @return Style
  104. */
  105. public function getSharedComponent()
  106. {
  107. $activeSheet = $this->getActiveSheet();
  108. $selectedCell = $this->getActiveCell(); // e.g. 'A1'
  109. if ($activeSheet->cellExists($selectedCell)) {
  110. $xfIndex = $activeSheet->getCell($selectedCell)->getXfIndex();
  111. } else {
  112. $xfIndex = 0;
  113. }
  114. return $this->parent->getCellXfByIndex($xfIndex);
  115. }
  116. /**
  117. * Get parent. Only used for style supervisor.
  118. *
  119. * @return Spreadsheet
  120. */
  121. public function getParent()
  122. {
  123. return $this->parent;
  124. }
  125. /**
  126. * Build style array from subcomponents.
  127. *
  128. * @param array $array
  129. *
  130. * @return array
  131. */
  132. public function getStyleArray($array)
  133. {
  134. return ['quotePrefix' => $array];
  135. }
  136. /**
  137. * Apply styles from array.
  138. *
  139. * <code>
  140. * $spreadsheet->getActiveSheet()->getStyle('B2')->applyFromArray(
  141. * [
  142. * 'font' => [
  143. * 'name' => 'Arial',
  144. * 'bold' => true,
  145. * 'italic' => false,
  146. * 'underline' => Font::UNDERLINE_DOUBLE,
  147. * 'strikethrough' => false,
  148. * 'color' => [
  149. * 'rgb' => '808080'
  150. * ]
  151. * ],
  152. * 'borders' => [
  153. * 'bottom' => [
  154. * 'borderStyle' => Border::BORDER_DASHDOT,
  155. * 'color' => [
  156. * 'rgb' => '808080'
  157. * ]
  158. * ],
  159. * 'top' => [
  160. * 'borderStyle' => Border::BORDER_DASHDOT,
  161. * 'color' => [
  162. * 'rgb' => '808080'
  163. * ]
  164. * ]
  165. * ],
  166. * 'alignment' => [
  167. * 'horizontal' => Alignment::HORIZONTAL_CENTER,
  168. * 'vertical' => Alignment::VERTICAL_CENTER,
  169. * 'wrapText' => true,
  170. * ],
  171. * 'quotePrefix' => true
  172. * ]
  173. * );
  174. * </code>
  175. *
  176. * @param array $pStyles Array containing style information
  177. * @param bool $pAdvanced advanced mode for setting borders
  178. *
  179. * @return Style
  180. */
  181. public function applyFromArray(array $pStyles, $pAdvanced = true)
  182. {
  183. if ($this->isSupervisor) {
  184. $pRange = $this->getSelectedCells();
  185. // Uppercase coordinate
  186. $pRange = strtoupper($pRange);
  187. // Is it a cell range or a single cell?
  188. if (strpos($pRange, ':') === false) {
  189. $rangeA = $pRange;
  190. $rangeB = $pRange;
  191. } else {
  192. list($rangeA, $rangeB) = explode(':', $pRange);
  193. }
  194. // Calculate range outer borders
  195. $rangeStart = Coordinate::coordinateFromString($rangeA);
  196. $rangeEnd = Coordinate::coordinateFromString($rangeB);
  197. // Translate column into index
  198. $rangeStart[0] = Coordinate::columnIndexFromString($rangeStart[0]);
  199. $rangeEnd[0] = Coordinate::columnIndexFromString($rangeEnd[0]);
  200. // Make sure we can loop upwards on rows and columns
  201. if ($rangeStart[0] > $rangeEnd[0] && $rangeStart[1] > $rangeEnd[1]) {
  202. $tmp = $rangeStart;
  203. $rangeStart = $rangeEnd;
  204. $rangeEnd = $tmp;
  205. }
  206. // ADVANCED MODE:
  207. if ($pAdvanced && isset($pStyles['borders'])) {
  208. // 'allBorders' is a shorthand property for 'outline' and 'inside' and
  209. // it applies to components that have not been set explicitly
  210. if (isset($pStyles['borders']['allBorders'])) {
  211. foreach (['outline', 'inside'] as $component) {
  212. if (!isset($pStyles['borders'][$component])) {
  213. $pStyles['borders'][$component] = $pStyles['borders']['allBorders'];
  214. }
  215. }
  216. unset($pStyles['borders']['allBorders']); // not needed any more
  217. }
  218. // 'outline' is a shorthand property for 'top', 'right', 'bottom', 'left'
  219. // it applies to components that have not been set explicitly
  220. if (isset($pStyles['borders']['outline'])) {
  221. foreach (['top', 'right', 'bottom', 'left'] as $component) {
  222. if (!isset($pStyles['borders'][$component])) {
  223. $pStyles['borders'][$component] = $pStyles['borders']['outline'];
  224. }
  225. }
  226. unset($pStyles['borders']['outline']); // not needed any more
  227. }
  228. // 'inside' is a shorthand property for 'vertical' and 'horizontal'
  229. // it applies to components that have not been set explicitly
  230. if (isset($pStyles['borders']['inside'])) {
  231. foreach (['vertical', 'horizontal'] as $component) {
  232. if (!isset($pStyles['borders'][$component])) {
  233. $pStyles['borders'][$component] = $pStyles['borders']['inside'];
  234. }
  235. }
  236. unset($pStyles['borders']['inside']); // not needed any more
  237. }
  238. // width and height characteristics of selection, 1, 2, or 3 (for 3 or more)
  239. $xMax = min($rangeEnd[0] - $rangeStart[0] + 1, 3);
  240. $yMax = min($rangeEnd[1] - $rangeStart[1] + 1, 3);
  241. // loop through up to 3 x 3 = 9 regions
  242. for ($x = 1; $x <= $xMax; ++$x) {
  243. // start column index for region
  244. $colStart = ($x == 3) ?
  245. Coordinate::stringFromColumnIndex($rangeEnd[0])
  246. : Coordinate::stringFromColumnIndex($rangeStart[0] + $x - 1);
  247. // end column index for region
  248. $colEnd = ($x == 1) ?
  249. Coordinate::stringFromColumnIndex($rangeStart[0])
  250. : Coordinate::stringFromColumnIndex($rangeEnd[0] - $xMax + $x);
  251. for ($y = 1; $y <= $yMax; ++$y) {
  252. // which edges are touching the region
  253. $edges = [];
  254. if ($x == 1) {
  255. // are we at left edge
  256. $edges[] = 'left';
  257. }
  258. if ($x == $xMax) {
  259. // are we at right edge
  260. $edges[] = 'right';
  261. }
  262. if ($y == 1) {
  263. // are we at top edge?
  264. $edges[] = 'top';
  265. }
  266. if ($y == $yMax) {
  267. // are we at bottom edge?
  268. $edges[] = 'bottom';
  269. }
  270. // start row index for region
  271. $rowStart = ($y == 3) ?
  272. $rangeEnd[1] : $rangeStart[1] + $y - 1;
  273. // end row index for region
  274. $rowEnd = ($y == 1) ?
  275. $rangeStart[1] : $rangeEnd[1] - $yMax + $y;
  276. // build range for region
  277. $range = $colStart . $rowStart . ':' . $colEnd . $rowEnd;
  278. // retrieve relevant style array for region
  279. $regionStyles = $pStyles;
  280. unset($regionStyles['borders']['inside']);
  281. // what are the inner edges of the region when looking at the selection
  282. $innerEdges = array_diff(['top', 'right', 'bottom', 'left'], $edges);
  283. // inner edges that are not touching the region should take the 'inside' border properties if they have been set
  284. foreach ($innerEdges as $innerEdge) {
  285. switch ($innerEdge) {
  286. case 'top':
  287. case 'bottom':
  288. // should pick up 'horizontal' border property if set
  289. if (isset($pStyles['borders']['horizontal'])) {
  290. $regionStyles['borders'][$innerEdge] = $pStyles['borders']['horizontal'];
  291. } else {
  292. unset($regionStyles['borders'][$innerEdge]);
  293. }
  294. break;
  295. case 'left':
  296. case 'right':
  297. // should pick up 'vertical' border property if set
  298. if (isset($pStyles['borders']['vertical'])) {
  299. $regionStyles['borders'][$innerEdge] = $pStyles['borders']['vertical'];
  300. } else {
  301. unset($regionStyles['borders'][$innerEdge]);
  302. }
  303. break;
  304. }
  305. }
  306. // apply region style to region by calling applyFromArray() in simple mode
  307. $this->getActiveSheet()->getStyle($range)->applyFromArray($regionStyles, false);
  308. }
  309. }
  310. // restore initial cell selection range
  311. $this->getActiveSheet()->getStyle($pRange);
  312. return $this;
  313. }
  314. // SIMPLE MODE:
  315. // Selection type, inspect
  316. if (preg_match('/^[A-Z]+1:[A-Z]+1048576$/', $pRange)) {
  317. $selectionType = 'COLUMN';
  318. } elseif (preg_match('/^A\d+:XFD\d+$/', $pRange)) {
  319. $selectionType = 'ROW';
  320. } else {
  321. $selectionType = 'CELL';
  322. }
  323. // First loop through columns, rows, or cells to find out which styles are affected by this operation
  324. switch ($selectionType) {
  325. case 'COLUMN':
  326. $oldXfIndexes = [];
  327. for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) {
  328. $oldXfIndexes[$this->getActiveSheet()->getColumnDimensionByColumn($col)->getXfIndex()] = true;
  329. }
  330. break;
  331. case 'ROW':
  332. $oldXfIndexes = [];
  333. for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) {
  334. if ($this->getActiveSheet()->getRowDimension($row)->getXfIndex() == null) {
  335. $oldXfIndexes[0] = true; // row without explicit style should be formatted based on default style
  336. } else {
  337. $oldXfIndexes[$this->getActiveSheet()->getRowDimension($row)->getXfIndex()] = true;
  338. }
  339. }
  340. break;
  341. case 'CELL':
  342. $oldXfIndexes = [];
  343. for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) {
  344. for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) {
  345. $oldXfIndexes[$this->getActiveSheet()->getCellByColumnAndRow($col, $row)->getXfIndex()] = true;
  346. }
  347. }
  348. break;
  349. }
  350. // clone each of the affected styles, apply the style array, and add the new styles to the workbook
  351. $workbook = $this->getActiveSheet()->getParent();
  352. foreach ($oldXfIndexes as $oldXfIndex => $dummy) {
  353. $style = $workbook->getCellXfByIndex($oldXfIndex);
  354. $newStyle = clone $style;
  355. $newStyle->applyFromArray($pStyles);
  356. if ($existingStyle = $workbook->getCellXfByHashCode($newStyle->getHashCode())) {
  357. // there is already such cell Xf in our collection
  358. $newXfIndexes[$oldXfIndex] = $existingStyle->getIndex();
  359. } else {
  360. // we don't have such a cell Xf, need to add
  361. $workbook->addCellXf($newStyle);
  362. $newXfIndexes[$oldXfIndex] = $newStyle->getIndex();
  363. }
  364. }
  365. // Loop through columns, rows, or cells again and update the XF index
  366. switch ($selectionType) {
  367. case 'COLUMN':
  368. for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) {
  369. $columnDimension = $this->getActiveSheet()->getColumnDimensionByColumn($col);
  370. $oldXfIndex = $columnDimension->getXfIndex();
  371. $columnDimension->setXfIndex($newXfIndexes[$oldXfIndex]);
  372. }
  373. break;
  374. case 'ROW':
  375. for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) {
  376. $rowDimension = $this->getActiveSheet()->getRowDimension($row);
  377. $oldXfIndex = $rowDimension->getXfIndex() === null ?
  378. 0 : $rowDimension->getXfIndex(); // row without explicit style should be formatted based on default style
  379. $rowDimension->setXfIndex($newXfIndexes[$oldXfIndex]);
  380. }
  381. break;
  382. case 'CELL':
  383. for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) {
  384. for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) {
  385. $cell = $this->getActiveSheet()->getCellByColumnAndRow($col, $row);
  386. $oldXfIndex = $cell->getXfIndex();
  387. $cell->setXfIndex($newXfIndexes[$oldXfIndex]);
  388. }
  389. }
  390. break;
  391. }
  392. } else {
  393. // not a supervisor, just apply the style array directly on style object
  394. if (isset($pStyles['fill'])) {
  395. $this->getFill()->applyFromArray($pStyles['fill']);
  396. }
  397. if (isset($pStyles['font'])) {
  398. $this->getFont()->applyFromArray($pStyles['font']);
  399. }
  400. if (isset($pStyles['borders'])) {
  401. $this->getBorders()->applyFromArray($pStyles['borders']);
  402. }
  403. if (isset($pStyles['alignment'])) {
  404. $this->getAlignment()->applyFromArray($pStyles['alignment']);
  405. }
  406. if (isset($pStyles['numberFormat'])) {
  407. $this->getNumberFormat()->applyFromArray($pStyles['numberFormat']);
  408. }
  409. if (isset($pStyles['protection'])) {
  410. $this->getProtection()->applyFromArray($pStyles['protection']);
  411. }
  412. if (isset($pStyles['quotePrefix'])) {
  413. $this->quotePrefix = $pStyles['quotePrefix'];
  414. }
  415. }
  416. return $this;
  417. }
  418. /**
  419. * Get Fill.
  420. *
  421. * @return Fill
  422. */
  423. public function getFill()
  424. {
  425. return $this->fill;
  426. }
  427. /**
  428. * Get Font.
  429. *
  430. * @return Font
  431. */
  432. public function getFont()
  433. {
  434. return $this->font;
  435. }
  436. /**
  437. * Set font.
  438. *
  439. * @param Font $font
  440. *
  441. * @return Style
  442. */
  443. public function setFont(Font $font)
  444. {
  445. $this->font = $font;
  446. return $this;
  447. }
  448. /**
  449. * Get Borders.
  450. *
  451. * @return Borders
  452. */
  453. public function getBorders()
  454. {
  455. return $this->borders;
  456. }
  457. /**
  458. * Get Alignment.
  459. *
  460. * @return Alignment
  461. */
  462. public function getAlignment()
  463. {
  464. return $this->alignment;
  465. }
  466. /**
  467. * Get Number Format.
  468. *
  469. * @return NumberFormat
  470. */
  471. public function getNumberFormat()
  472. {
  473. return $this->numberFormat;
  474. }
  475. /**
  476. * Get Conditional Styles. Only used on supervisor.
  477. *
  478. * @return Conditional[]
  479. */
  480. public function getConditionalStyles()
  481. {
  482. return $this->getActiveSheet()->getConditionalStyles($this->getActiveCell());
  483. }
  484. /**
  485. * Set Conditional Styles. Only used on supervisor.
  486. *
  487. * @param Conditional[] $pValue Array of conditional styles
  488. *
  489. * @return Style
  490. */
  491. public function setConditionalStyles(array $pValue)
  492. {
  493. $this->getActiveSheet()->setConditionalStyles($this->getSelectedCells(), $pValue);
  494. return $this;
  495. }
  496. /**
  497. * Get Protection.
  498. *
  499. * @return Protection
  500. */
  501. public function getProtection()
  502. {
  503. return $this->protection;
  504. }
  505. /**
  506. * Get quote prefix.
  507. *
  508. * @return bool
  509. */
  510. public function getQuotePrefix()
  511. {
  512. if ($this->isSupervisor) {
  513. return $this->getSharedComponent()->getQuotePrefix();
  514. }
  515. return $this->quotePrefix;
  516. }
  517. /**
  518. * Set quote prefix.
  519. *
  520. * @param bool $pValue
  521. *
  522. * @return Style
  523. */
  524. public function setQuotePrefix($pValue)
  525. {
  526. if ($pValue == '') {
  527. $pValue = false;
  528. }
  529. if ($this->isSupervisor) {
  530. $styleArray = ['quotePrefix' => $pValue];
  531. $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray);
  532. } else {
  533. $this->quotePrefix = (bool) $pValue;
  534. }
  535. return $this;
  536. }
  537. /**
  538. * Get hash code.
  539. *
  540. * @return string Hash code
  541. */
  542. public function getHashCode()
  543. {
  544. $hashConditionals = '';
  545. foreach ($this->conditionalStyles as $conditional) {
  546. $hashConditionals .= $conditional->getHashCode();
  547. }
  548. return md5(
  549. $this->fill->getHashCode() .
  550. $this->font->getHashCode() .
  551. $this->borders->getHashCode() .
  552. $this->alignment->getHashCode() .
  553. $this->numberFormat->getHashCode() .
  554. $hashConditionals .
  555. $this->protection->getHashCode() .
  556. ($this->quotePrefix ? 't' : 'f') .
  557. __CLASS__
  558. );
  559. }
  560. /**
  561. * Get own index in style collection.
  562. *
  563. * @return int
  564. */
  565. public function getIndex()
  566. {
  567. return $this->index;
  568. }
  569. /**
  570. * Set own index in style collection.
  571. *
  572. * @param int $pValue
  573. */
  574. public function setIndex($pValue)
  575. {
  576. $this->index = $pValue;
  577. }
  578. }