Style.php 21 KB

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