Style.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633
  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. * array(
  135. * 'font' => array(
  136. * 'name' => 'Arial',
  137. * 'bold' => true,
  138. * 'italic' => false,
  139. * 'underline' => Font::UNDERLINE_DOUBLE,
  140. * 'strikethrough' => false,
  141. * 'color' => array(
  142. * 'rgb' => '808080'
  143. * )
  144. * ),
  145. * 'borders' => array(
  146. * 'bottom' => array(
  147. * 'borderStyle' => Border::BORDER_DASHDOT,
  148. * 'color' => array(
  149. * 'rgb' => '808080'
  150. * )
  151. * ),
  152. * 'top' => array(
  153. * 'borderStyle' => Border::BORDER_DASHDOT,
  154. * 'color' => array(
  155. * 'rgb' => '808080'
  156. * )
  157. * )
  158. * ),
  159. * 'quotePrefix' => true
  160. * )
  161. * );
  162. * </code>
  163. *
  164. * @param array $pStyles Array containing style information
  165. * @param bool $pAdvanced advanced mode for setting borders
  166. *
  167. * @return Style
  168. */
  169. public function applyFromArray(array $pStyles, $pAdvanced = true)
  170. {
  171. if ($this->isSupervisor) {
  172. $pRange = $this->getSelectedCells();
  173. // Uppercase coordinate
  174. $pRange = strtoupper($pRange);
  175. // Is it a cell range or a single cell?
  176. if (strpos($pRange, ':') === false) {
  177. $rangeA = $pRange;
  178. $rangeB = $pRange;
  179. } else {
  180. list($rangeA, $rangeB) = explode(':', $pRange);
  181. }
  182. // Calculate range outer borders
  183. $rangeStart = Coordinate::coordinateFromString($rangeA);
  184. $rangeEnd = Coordinate::coordinateFromString($rangeB);
  185. // Translate column into index
  186. $rangeStart[0] = Coordinate::columnIndexFromString($rangeStart[0]);
  187. $rangeEnd[0] = Coordinate::columnIndexFromString($rangeEnd[0]);
  188. // Make sure we can loop upwards on rows and columns
  189. if ($rangeStart[0] > $rangeEnd[0] && $rangeStart[1] > $rangeEnd[1]) {
  190. $tmp = $rangeStart;
  191. $rangeStart = $rangeEnd;
  192. $rangeEnd = $tmp;
  193. }
  194. // ADVANCED MODE:
  195. if ($pAdvanced && isset($pStyles['borders'])) {
  196. // 'allBorders' is a shorthand property for 'outline' and 'inside' and
  197. // it applies to components that have not been set explicitly
  198. if (isset($pStyles['borders']['allBorders'])) {
  199. foreach (['outline', 'inside'] as $component) {
  200. if (!isset($pStyles['borders'][$component])) {
  201. $pStyles['borders'][$component] = $pStyles['borders']['allBorders'];
  202. }
  203. }
  204. unset($pStyles['borders']['allBorders']); // not needed any more
  205. }
  206. // 'outline' is a shorthand property for 'top', 'right', 'bottom', 'left'
  207. // it applies to components that have not been set explicitly
  208. if (isset($pStyles['borders']['outline'])) {
  209. foreach (['top', 'right', 'bottom', 'left'] as $component) {
  210. if (!isset($pStyles['borders'][$component])) {
  211. $pStyles['borders'][$component] = $pStyles['borders']['outline'];
  212. }
  213. }
  214. unset($pStyles['borders']['outline']); // not needed any more
  215. }
  216. // 'inside' is a shorthand property for 'vertical' and 'horizontal'
  217. // it applies to components that have not been set explicitly
  218. if (isset($pStyles['borders']['inside'])) {
  219. foreach (['vertical', 'horizontal'] as $component) {
  220. if (!isset($pStyles['borders'][$component])) {
  221. $pStyles['borders'][$component] = $pStyles['borders']['inside'];
  222. }
  223. }
  224. unset($pStyles['borders']['inside']); // not needed any more
  225. }
  226. // width and height characteristics of selection, 1, 2, or 3 (for 3 or more)
  227. $xMax = min($rangeEnd[0] - $rangeStart[0] + 1, 3);
  228. $yMax = min($rangeEnd[1] - $rangeStart[1] + 1, 3);
  229. // loop through up to 3 x 3 = 9 regions
  230. for ($x = 1; $x <= $xMax; ++$x) {
  231. // start column index for region
  232. $colStart = ($x == 3) ?
  233. Coordinate::stringFromColumnIndex($rangeEnd[0])
  234. : Coordinate::stringFromColumnIndex($rangeStart[0] + $x - 1);
  235. // end column index for region
  236. $colEnd = ($x == 1) ?
  237. Coordinate::stringFromColumnIndex($rangeStart[0])
  238. : Coordinate::stringFromColumnIndex($rangeEnd[0] - $xMax + $x);
  239. for ($y = 1; $y <= $yMax; ++$y) {
  240. // which edges are touching the region
  241. $edges = [];
  242. if ($x == 1) {
  243. // are we at left edge
  244. $edges[] = 'left';
  245. }
  246. if ($x == $xMax) {
  247. // are we at right edge
  248. $edges[] = 'right';
  249. }
  250. if ($y == 1) {
  251. // are we at top edge?
  252. $edges[] = 'top';
  253. }
  254. if ($y == $yMax) {
  255. // are we at bottom edge?
  256. $edges[] = 'bottom';
  257. }
  258. // start row index for region
  259. $rowStart = ($y == 3) ?
  260. $rangeEnd[1] : $rangeStart[1] + $y - 1;
  261. // end row index for region
  262. $rowEnd = ($y == 1) ?
  263. $rangeStart[1] : $rangeEnd[1] - $yMax + $y;
  264. // build range for region
  265. $range = $colStart . $rowStart . ':' . $colEnd . $rowEnd;
  266. // retrieve relevant style array for region
  267. $regionStyles = $pStyles;
  268. unset($regionStyles['borders']['inside']);
  269. // what are the inner edges of the region when looking at the selection
  270. $innerEdges = array_diff(['top', 'right', 'bottom', 'left'], $edges);
  271. // inner edges that are not touching the region should take the 'inside' border properties if they have been set
  272. foreach ($innerEdges as $innerEdge) {
  273. switch ($innerEdge) {
  274. case 'top':
  275. case 'bottom':
  276. // should pick up 'horizontal' border property if set
  277. if (isset($pStyles['borders']['horizontal'])) {
  278. $regionStyles['borders'][$innerEdge] = $pStyles['borders']['horizontal'];
  279. } else {
  280. unset($regionStyles['borders'][$innerEdge]);
  281. }
  282. break;
  283. case 'left':
  284. case 'right':
  285. // should pick up 'vertical' border property if set
  286. if (isset($pStyles['borders']['vertical'])) {
  287. $regionStyles['borders'][$innerEdge] = $pStyles['borders']['vertical'];
  288. } else {
  289. unset($regionStyles['borders'][$innerEdge]);
  290. }
  291. break;
  292. }
  293. }
  294. // apply region style to region by calling applyFromArray() in simple mode
  295. $this->getActiveSheet()->getStyle($range)->applyFromArray($regionStyles, false);
  296. }
  297. }
  298. return $this;
  299. }
  300. // SIMPLE MODE:
  301. // Selection type, inspect
  302. if (preg_match('/^[A-Z]+1:[A-Z]+1048576$/', $pRange)) {
  303. $selectionType = 'COLUMN';
  304. } elseif (preg_match('/^A\d+:XFD\d+$/', $pRange)) {
  305. $selectionType = 'ROW';
  306. } else {
  307. $selectionType = 'CELL';
  308. }
  309. // First loop through columns, rows, or cells to find out which styles are affected by this operation
  310. switch ($selectionType) {
  311. case 'COLUMN':
  312. $oldXfIndexes = [];
  313. for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) {
  314. $oldXfIndexes[$this->getActiveSheet()->getColumnDimensionByColumn($col)->getXfIndex()] = true;
  315. }
  316. break;
  317. case 'ROW':
  318. $oldXfIndexes = [];
  319. for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) {
  320. if ($this->getActiveSheet()->getRowDimension($row)->getXfIndex() == null) {
  321. $oldXfIndexes[0] = true; // row without explicit style should be formatted based on default style
  322. } else {
  323. $oldXfIndexes[$this->getActiveSheet()->getRowDimension($row)->getXfIndex()] = true;
  324. }
  325. }
  326. break;
  327. case 'CELL':
  328. $oldXfIndexes = [];
  329. for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) {
  330. for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) {
  331. $oldXfIndexes[$this->getActiveSheet()->getCellByColumnAndRow($col, $row)->getXfIndex()] = true;
  332. }
  333. }
  334. break;
  335. }
  336. // clone each of the affected styles, apply the style array, and add the new styles to the workbook
  337. $workbook = $this->getActiveSheet()->getParent();
  338. foreach ($oldXfIndexes as $oldXfIndex => $dummy) {
  339. $style = $workbook->getCellXfByIndex($oldXfIndex);
  340. $newStyle = clone $style;
  341. $newStyle->applyFromArray($pStyles);
  342. if ($existingStyle = $workbook->getCellXfByHashCode($newStyle->getHashCode())) {
  343. // there is already such cell Xf in our collection
  344. $newXfIndexes[$oldXfIndex] = $existingStyle->getIndex();
  345. } else {
  346. // we don't have such a cell Xf, need to add
  347. $workbook->addCellXf($newStyle);
  348. $newXfIndexes[$oldXfIndex] = $newStyle->getIndex();
  349. }
  350. }
  351. // Loop through columns, rows, or cells again and update the XF index
  352. switch ($selectionType) {
  353. case 'COLUMN':
  354. for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) {
  355. $columnDimension = $this->getActiveSheet()->getColumnDimensionByColumn($col);
  356. $oldXfIndex = $columnDimension->getXfIndex();
  357. $columnDimension->setXfIndex($newXfIndexes[$oldXfIndex]);
  358. }
  359. break;
  360. case 'ROW':
  361. for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) {
  362. $rowDimension = $this->getActiveSheet()->getRowDimension($row);
  363. $oldXfIndex = $rowDimension->getXfIndex() === null ?
  364. 0 : $rowDimension->getXfIndex(); // row without explicit style should be formatted based on default style
  365. $rowDimension->setXfIndex($newXfIndexes[$oldXfIndex]);
  366. }
  367. break;
  368. case 'CELL':
  369. for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) {
  370. for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) {
  371. $cell = $this->getActiveSheet()->getCellByColumnAndRow($col, $row);
  372. $oldXfIndex = $cell->getXfIndex();
  373. $cell->setXfIndex($newXfIndexes[$oldXfIndex]);
  374. }
  375. }
  376. break;
  377. }
  378. } else {
  379. // not a supervisor, just apply the style array directly on style object
  380. if (isset($pStyles['fill'])) {
  381. $this->getFill()->applyFromArray($pStyles['fill']);
  382. }
  383. if (isset($pStyles['font'])) {
  384. $this->getFont()->applyFromArray($pStyles['font']);
  385. }
  386. if (isset($pStyles['borders'])) {
  387. $this->getBorders()->applyFromArray($pStyles['borders']);
  388. }
  389. if (isset($pStyles['alignment'])) {
  390. $this->getAlignment()->applyFromArray($pStyles['alignment']);
  391. }
  392. if (isset($pStyles['numberFormat'])) {
  393. $this->getNumberFormat()->applyFromArray($pStyles['numberFormat']);
  394. }
  395. if (isset($pStyles['protection'])) {
  396. $this->getProtection()->applyFromArray($pStyles['protection']);
  397. }
  398. if (isset($pStyles['quotePrefix'])) {
  399. $this->quotePrefix = $pStyles['quotePrefix'];
  400. }
  401. }
  402. return $this;
  403. }
  404. /**
  405. * Get Fill.
  406. *
  407. * @return Fill
  408. */
  409. public function getFill()
  410. {
  411. return $this->fill;
  412. }
  413. /**
  414. * Get Font.
  415. *
  416. * @return Font
  417. */
  418. public function getFont()
  419. {
  420. return $this->font;
  421. }
  422. /**
  423. * Set font.
  424. *
  425. * @param Font $font
  426. *
  427. * @return Style
  428. */
  429. public function setFont(Font $font)
  430. {
  431. $this->font = $font;
  432. return $this;
  433. }
  434. /**
  435. * Get Borders.
  436. *
  437. * @return Borders
  438. */
  439. public function getBorders()
  440. {
  441. return $this->borders;
  442. }
  443. /**
  444. * Get Alignment.
  445. *
  446. * @return Alignment
  447. */
  448. public function getAlignment()
  449. {
  450. return $this->alignment;
  451. }
  452. /**
  453. * Get Number Format.
  454. *
  455. * @return NumberFormat
  456. */
  457. public function getNumberFormat()
  458. {
  459. return $this->numberFormat;
  460. }
  461. /**
  462. * Get Conditional Styles. Only used on supervisor.
  463. *
  464. * @return Conditional[]
  465. */
  466. public function getConditionalStyles()
  467. {
  468. return $this->getActiveSheet()->getConditionalStyles($this->getActiveCell());
  469. }
  470. /**
  471. * Set Conditional Styles. Only used on supervisor.
  472. *
  473. * @param Conditional[] $pValue Array of conditional styles
  474. *
  475. * @return Style
  476. */
  477. public function setConditionalStyles(array $pValue)
  478. {
  479. $this->getActiveSheet()->setConditionalStyles($this->getSelectedCells(), $pValue);
  480. return $this;
  481. }
  482. /**
  483. * Get Protection.
  484. *
  485. * @return Protection
  486. */
  487. public function getProtection()
  488. {
  489. return $this->protection;
  490. }
  491. /**
  492. * Get quote prefix.
  493. *
  494. * @return bool
  495. */
  496. public function getQuotePrefix()
  497. {
  498. if ($this->isSupervisor) {
  499. return $this->getSharedComponent()->getQuotePrefix();
  500. }
  501. return $this->quotePrefix;
  502. }
  503. /**
  504. * Set quote prefix.
  505. *
  506. * @param bool $pValue
  507. *
  508. * @return Style
  509. */
  510. public function setQuotePrefix($pValue)
  511. {
  512. if ($pValue == '') {
  513. $pValue = false;
  514. }
  515. if ($this->isSupervisor) {
  516. $styleArray = ['quotePrefix' => $pValue];
  517. $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray);
  518. } else {
  519. $this->quotePrefix = (bool) $pValue;
  520. }
  521. return $this;
  522. }
  523. /**
  524. * Get hash code.
  525. *
  526. * @return string Hash code
  527. */
  528. public function getHashCode()
  529. {
  530. $hashConditionals = '';
  531. foreach ($this->conditionalStyles as $conditional) {
  532. $hashConditionals .= $conditional->getHashCode();
  533. }
  534. return md5(
  535. $this->fill->getHashCode() .
  536. $this->font->getHashCode() .
  537. $this->borders->getHashCode() .
  538. $this->alignment->getHashCode() .
  539. $this->numberFormat->getHashCode() .
  540. $hashConditionals .
  541. $this->protection->getHashCode() .
  542. ($this->quotePrefix ? 't' : 'f') .
  543. __CLASS__
  544. );
  545. }
  546. /**
  547. * Get own index in style collection.
  548. *
  549. * @return int
  550. */
  551. public function getIndex()
  552. {
  553. return $this->index;
  554. }
  555. /**
  556. * Set own index in style collection.
  557. *
  558. * @param int $pValue
  559. */
  560. public function setIndex($pValue)
  561. {
  562. $this->index = $pValue;
  563. }
  564. }