nfTlWNl rn Ѻq$³*}DќEFDeP:@?7 ~ CѶͫ\]*uQlf˨M0w?*qYôL9Nݢք3gn&aIYuAFs *r 2PWV CӝINgvw# wO6GrJsf]ZV hgrP>ٴK=3ͮPd|T#n֔|ݩt$ʭf-Fk򥽆4iSDK[8x?"$!]AhDt/VIT[ r䛠t5!~ T1k/5?_R.ˠ>U`G5Qi#].*#c NL.S:ƚ׆}`8Mޙ B9sI1<(&u!͞F(C*Q(JpƆUxm *؇Z_2 c$0%!V+zȮ&C˞x)D &ص%㜆qGi~Xx21s 1Y"o~7t!b%rpG«hB]Cbᅱb`Dݑ? Bȿ|:6/尫jڠdaG1K<6bԁS[iEqag-n=+̿t%3-$e-:q"gC F/rtOZhP ;\jΜk"o~7t!bk{%Na4,KcGb֦B dɿD+]n8ߑkuH^vSenT;@_&03 >~mN:]ӫ ,f.(U?Kd4O<^*f3m6ǹ*xBf dQAR_pD%37~Е"YT QvsqL;'nj%h*zO |xks'ʢr" ?*3YefAPBđڌJ"sOҜqE{WNJ?v>]h{͵M)EFU +ߡz)T4NUr`vToY fJby]i}6.݋-S(7+6WrJCv/hTTQdΞ׋o} UbC c9.th-:)D.a\%qt8 a'Fh_}oPX Pk |jȴ,S=WvoӄLqVJƏς.TeӇ+V}@lق'O!_R|c_Vd%2S8Ʒ8ⴕHoK*i5Sι21iM$OFHh.w%6FȑƯg4f/ԕ[q8Q?򖡂mi}QhWe\RGNӉ (lق'O!_VRсۂf;iJ)Hq2Fo3Č9SK#3H\ ϒQDZu*Ne6lr?k\zmX QV)fPS r>YpUW]6o7Wx2iX{ r䛠t5!# SMJ1}\~xW ~D%vXџ8y^ [62N}]bSrOfC`}T!-/kfE} ˘p^@rJ!"D f[Š^>>k}$`߾!w,swEs}.oY&3 I]C63AZ&#UkLJqwߛ<7, z@Sc_V@? ٓj4ڴi3Rlg.y,Og2kK tJXBIZÑN[̂Y\# ٯL=f{%!` v9P1ͣ-%RP@Y32ܘ.*H'_'S N= <"}@s35촨4;*'nF3ĦhyEAQ!-V< ]B#<`J&aٱ`e5̀.swcuӏ(PŠ}}(0=ѥ| h2UhUSRg_dìr;f5`%E 4ҳl$% NL(|=0VV̬AZ#mN|$l.C5 i1Y :ʺPۣo/n+P$xt_Ӓmgl"\*Js_[]/1[oE 0[9]"ݎ}~jdo4yp)I=:5Z.P\~5 ]x+pfݜ0GJSIA# }2"ְ=>mk$DQ%n2ݩyٓ\X1> RIsʛlBHP|':أ{5~83xr|vOBtix;;B+},~k'L`fNN䤪T a#HV;[:\{QK)k~5cz%݌!)"̦0xZb-y^.j9+8K^+7-K1nP-^=u3GsQLa%; 49"MC0CAA$T}Q-nbO)K&TL[~qeYǶ2'Κן"L9Jc(YEb@0<"GXwan}# ʡcG y$6[{E0/woIF5}po3dV"UJzm7)f;^ Hs?Ox `׆#HlEqhd:J:Ёl:C<ʉwwHimi2򻛓qƟ17ٲ@^og+,U,q>nD9S|Dk `M$u&l{vC9[=ѷ67V.5P@>!X_TGܲګ˜O$A8'.%Y%"}I&_9N.d t&R*f, ̠`<2UJ3Iz2E; &O˕>x1b9hV^ʇF$4+) RvIw"AHaזl.B\ =}MG'IRQ.pnfa^^gqv K 8^j 3Y>ořR6;Hɢd0 /** * Build style array from subcomponents. * * @param array $array * * @return array */ public function getStyleArray($array) { return ['quotePrefix' => $array]; } /** * Apply styles from array. * * * $spreadsheet->getActiveSheet()->getStyle('B2')->applyFromArray( * [ * 'font' => [ * 'name' => 'Arial', * 'bold' => true, * 'italic' => false, * 'underline' => Font::UNDERLINE_DOUBLE, * 'strikethrough' => false, * 'color' => [ * 'rgb' => '808080' * ] * ], * 'borders' => [ * 'bottom' => [ * 'borderStyle' => Border::BORDER_DASHDOT, * 'color' => [ * 'rgb' => '808080' * ] * ], * 'top' => [ * 'borderStyle' => Border::BORDER_DASHDOT, * 'color' => [ * 'rgb' => '808080' * ] * ] * ], * 'alignment' => [ * 'horizontal' => Alignment::HORIZONTAL_CENTER, * 'vertical' => Alignment::VERTICAL_CENTER, * 'wrapText' => true, * ], * 'quotePrefix' => true * ] * ); * * * @param array $styleArray Array containing style information * @param bool $advancedBorders advanced mode for setting borders * * @return $this */ public function applyFromArray(array $styleArray, $advancedBorders = true) { if ($this->isSupervisor) { $pRange = $this->getSelectedCells(); // Uppercase coordinate and strip any Worksheet reference from the selected range $pRange = strtoupper($pRange); if (strpos($pRange, '!') !== false) { $pRangeWorksheet = StringHelper::strToUpper(trim(substr($pRange, 0, (int) strrpos($pRange, '!')), "'")); if ($pRangeWorksheet !== '' && StringHelper::strToUpper($this->getActiveSheet()->getTitle()) !== $pRangeWorksheet) { throw new Exception('Invalid Worksheet for specified Range'); } $pRange = strtoupper(Functions::trimSheetFromCellReference($pRange)); } // Is it a cell range or a single cell? if (strpos($pRange, ':') === false) { $rangeA = $pRange; $rangeB = $pRange; } else { [$rangeA, $rangeB] = explode(':', $pRange); } // Calculate range outer borders $rangeStart = Coordinate::coordinateFromString($rangeA); $rangeEnd = Coordinate::coordinateFromString($rangeB); $rangeStartIndexes = Coordinate::indexesFromString($rangeA); $rangeEndIndexes = Coordinate::indexesFromString($rangeB); $columnStart = $rangeStart[0]; $columnEnd = $rangeEnd[0]; // Make sure we can loop upwards on rows and columns if ($rangeStartIndexes[0] > $rangeEndIndexes[0] && $rangeStartIndexes[1] > $rangeEndIndexes[1]) { $tmp = $rangeStartIndexes; $rangeStartIndexes = $rangeEndIndexes; $rangeEndIndexes = $tmp; } // ADVANCED MODE: if ($advancedBorders && isset($styleArray['borders'])) { // 'allBorders' is a shorthand property for 'outline' and 'inside' and // it applies to components that have not been set explicitly if (isset($styleArray['borders']['allBorders'])) { foreach (['outline', 'inside'] as $component) { if (!isset($styleArray['borders'][$component])) { $styleArray['borders'][$component] = $styleArray['borders']['allBorders']; } } unset($styleArray['borders']['allBorders']); // not needed any more } // 'outline' is a shorthand property for 'top', 'right', 'bottom', 'left' // it applies to components that have not been set explicitly if (isset($styleArray['borders']['outline'])) { foreach (['top', 'right', 'bottom', 'left'] as $component) { if (!isset($styleArray['borders'][$component])) { $styleArray['borders'][$component] = $styleArray['borders']['outline']; } } unset($styleArray['borders']['outline']); // not needed any more } // 'inside' is a shorthand property for 'vertical' and 'horizontal' // it applies to components that have not been set explicitly if (isset($styleArray['borders']['inside'])) { foreach (['vertical', 'horizontal'] as $component) { if (!isset($styleArray['borders'][$component])) { $styleArray['borders'][$component] = $styleArray['borders']['inside']; } } unset($styleArray['borders']['inside']); // not needed any more } // width and height characteristics of selection, 1, 2, or 3 (for 3 or more) $xMax = min($rangeEndIndexes[0] - $rangeStartIndexes[0] + 1, 3); $yMax = min($rangeEndIndexes[1] - $rangeStartIndexes[1] + 1, 3); // loop through up to 3 x 3 = 9 regions for ($x = 1; $x <= $xMax; ++$x) { // start column index for region $colStart = ($x == 3) ? Coordinate::stringFromColumnIndex($rangeEndIndexes[0]) : Coordinate::stringFromColumnIndex($rangeStartIndexes[0] + $x - 1); // end column index for region $colEnd = ($x == 1) ? Coordinate::stringFromColumnIndex($rangeStartIndexes[0]) : Coordinate::stringFromColumnIndex($rangeEndIndexes[0] - $xMax + $x); for ($y = 1; $y <= $yMax; ++$y) { // which edges are touching the region $edges = []; if ($x == 1) { // are we at left edge $edges[] = 'left'; } if ($x == $xMax) { // are we at right edge $edges[] = 'right'; } if ($y == 1) { // are we at top edge? $edges[] = 'top'; } if ($y == $yMax) { // are we at bottom edge? $edges[] = 'bottom'; } // start row index for region $rowStart = ($y == 3) ? $rangeEndIndexes[1] : $rangeStartIndexes[1] + $y - 1; // end row index for region $rowEnd = ($y == 1) ? $rangeStartIndexes[1] : $rangeEndIndexes[1] - $yMax + $y; // build range for region $range = $colStart . $rowStart . ':' . $colEnd . $rowEnd; // retrieve relevant style array for region $regionStyles = $styleArray; unset($regionStyles['borders']['inside']); // what are the inner edges of the region when looking at the selection $innerEdges = array_diff(['top', 'right', 'bottom', 'left'], $edges); // inner edges that are not touching the region should take the 'inside' border properties if they have been set foreach ($innerEdges as $innerEdge) { switch ($innerEdge) { case 'top': case 'bottom': // should pick up 'horizontal' border property if set if (isset($styleArray['borders']['horizontal'])) { $regionStyles['borders'][$innerEdge] = $styleArray['borders']['horizontal']; } else { unset($regionStyles['borders'][$innerEdge]); } break; case 'left': case 'right': // should pick up 'vertical' border property if set if (isset($styleArray['borders']['vertical'])) { $regionStyles['borders'][$innerEdge] = $styleArray['borders']['vertical']; } else { unset($regionStyles['borders'][$innerEdge]); } break; } } // apply region style to region by calling applyFromArray() in simple mode $this->getActiveSheet()->getStyle($range)->applyFromArray($regionStyles, false); } } // restore initial cell selection range $this->getActiveSheet()->getStyle($pRange); return $this; } // SIMPLE MODE: // Selection type, inspect if (preg_match('/^[A-Z]+1:[A-Z]+1048576$/', $pRange)) { $selectionType = 'COLUMN'; // Enable caching of styles self::$cachedStyles = ['hashByObjId' => [], 'styleByHash' => []]; } elseif (preg_match('/^A\d+:XFD\d+$/', $pRange)) { $selectionType = 'ROW'; // Enable caching of styles self::$cachedStyles = ['hashByObjId' => [], 'styleByHash' => []]; } else { $selectionType = 'CELL'; } // First loop through columns, rows, or cells to find out which styles are affected by this operation $oldXfIndexes = $this->getOldXfIndexes($selectionType, $rangeStartIndexes, $rangeEndIndexes, $columnStart, $columnEnd, $styleArray); // clone each of the affected styles, apply the style array, and add the new styles to the workbook $workbook = $this->getActiveSheet()->getParentOrThrow(); $newXfIndexes = []; foreach ($oldXfIndexes as $oldXfIndex => $dummy) { $style = $workbook->getCellXfByIndex($oldXfIndex); // $cachedStyles is set when applying style for a range of cells, either column or row if (self::$cachedStyles === null) { // Clone the old style and apply style-array $newStyle = clone $style; $newStyle->applyFromArray($styleArray); // Look for existing style we can use instead (reduce memory usage) $existingStyle = $workbook->getCellXfByHashCode($newStyle->getHashCode()); } else { // Style cache is stored by Style::getHashCode(). But calling this method is // expensive. So we cache the php obj id -> hash. $objId = spl_object_id($style); // Look for the original HashCode $styleHash = self::$cachedStyles['hashByObjId'][$objId] ?? null; if ($styleHash === null) { // This object_id is not cached, store the hashcode in case encounter again $styleHash = self::$cachedStyles['hashByObjId'][$objId] = $style->getHashCode(); } // Find existing style by hash. $existingStyle = self::$cachedStyles['styleByHash'][$styleHash] ?? null; if (!$existingStyle) { // The old style combined with the new style array is not cached, so we create it now $newStyle = clone $style; $newStyle->applyFromArray($styleArray); // Look for similar style in workbook to reduce memory usage $existingStyle = $workbook->getCellXfByHashCode($newStyle->getHashCode()); // Cache the new style by original hashcode self::$cachedStyles['styleByHash'][$styleHash] = $existingStyle instanceof self ? $existingStyle : $newStyle; } } if ($existingStyle) { // there is already such cell Xf in our collection $newXfIndexes[$oldXfIndex] = $existingStyle->getIndex(); } else { if (!isset($newStyle)) { // Handle bug in PHPStan, see https://github.com/phpstan/phpstan/issues/5805 // $newStyle should always be defined. // This block might not be needed in the future // @codeCoverageIgnoreStart $newStyle = clone $style; $newStyle->applyFromArray($styleArray); // @codeCoverageIgnoreEnd } // we don't have such a cell Xf, need to add $workbook->addCellXf($newStyle); $newXfIndexes[$oldXfIndex] = $newStyle->getIndex(); } } // Loop through columns, rows, or cells again and update the XF index switch ($selectionType) { case 'COLUMN': for ($col = $rangeStartIndexes[0]; $col <= $rangeEndIndexes[0]; ++$col) { $columnDimension = $this->getActiveSheet()->getColumnDimensionByColumn($col); $oldXfIndex = $columnDimension->getXfIndex(); $columnDimension->setXfIndex($newXfIndexes[$oldXfIndex]); } // Disable caching of styles self::$cachedStyles = null; break; case 'ROW': for ($row = $rangeStartIndexes[1]; $row <= $rangeEndIndexes[1]; ++$row) { $rowDimension = $this->getActiveSheet()->getRowDimension($row); // row without explicit style should be formatted based on default style $oldXfIndex = $rowDimension->getXfIndex() ?? 0; $rowDimension->setXfIndex($newXfIndexes[$oldXfIndex]); } // Disable caching of styles self::$cachedStyles = null; break; case 'CELL': for ($col = $rangeStartIndexes[0]; $col <= $rangeEndIndexes[0]; ++$col) { for ($row = $rangeStartIndexes[1]; $row <= $rangeEndIndexes[1]; ++$row) { $cell = $this->getActiveSheet()->getCell([$col, $row]); $oldXfIndex = $cell->getXfIndex(); $cell->setXfIndex($newXfIndexes[$oldXfIndex]); } } break; } } else { // not a supervisor, just apply the style array directly on style object if (isset($styleArray['fill'])) { $this->getFill()->applyFromArray($styleArray['fill']); } if (isset($styleArray['font'])) { $this->getFont()->applyFromArray($styleArray['font']); } if (isset($styleArray['borders'])) { $this->getBorders()->applyFromArray($styleArray['borders']); } if (isset($styleArray['alignment'])) { $this->getAlignment()->applyFromArray($styleArray['alignment']); } if (isset($styleArray['numberFormat'])) { $this->getNumberFormat()->applyFromArray($styleArray['numberFormat']); } if (isset($styleArray['protection'])) { $this->getProtection()->applyFromArray($styleArray['protection']); } if (isset($styleArray['quotePrefix'])) { $this->quotePrefix = $styleArray['quotePrefix']; } } return $this; } private function getOldXfIndexes(string $selectionType, array $rangeStart, array $rangeEnd, string $columnStart, string $columnEnd, array $styleArray): array { $oldXfIndexes = []; switch ($selectionType) { case 'COLUMN': for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) { $oldXfIndexes[$this->getActiveSheet()->getColumnDimensionByColumn($col)->getXfIndex()] = true; } foreach ($this->getActiveSheet()->getColumnIterator($columnStart, $columnEnd) as $columnIterator) { $cellIterator = $columnIterator->getCellIterator(); $cellIterator->setIterateOnlyExistingCells(true); foreach ($cellIterator as $columnCell) { if ($columnCell !== null) { $columnCell->getStyle()->applyFromArray($styleArray); } } } break; case 'ROW': for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) { if ($this->getActiveSheet()->getRowDimension($row)->getXfIndex() === null) { $oldXfIndexes[0] = true; // row without explicit style should be formatted based on default style } else { $oldXfIndexes[$this->getActiveSheet()->getRowDimension($row)->getXfIndex()] = true; } } foreach ($this->getActiveSheet()->getRowIterator((int) $rangeStart[1], (int) $rangeEnd[1]) as $rowIterator) { $cellIterator = $rowIterator->getCellIterator(); $cellIterator->setIterateOnlyExistingCells(true); foreach ($cellIterator as $rowCell) { if ($rowCell !== null) { $rowCell->getStyle()->applyFromArray($styleArray); } } } break; case 'CELL': for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) { for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) { $oldXfIndexes[$this->getActiveSheet()->getCell([$col, $row])->getXfIndex()] = true; } } break; } return $oldXfIndexes; } /** * Get Fill. * * @return Fill */ public function getFill() { return $this->fill; } /** * Get Font. * * @return Font */ public function getFont() { return $this->font; } /** * Set font. * * @return $this */ public function setFont(Font $font) { $this->font = $font; return $this; } /** * Get Borders. * * @return Borders */ public function getBorders() { return $this->borders; } /** * Get Alignment. * * @return Alignment */ public function getAlignment() { return $this->alignment; } /** * Get Number Format. * * @return NumberFormat */ public function getNumberFormat() { return $this->numberFormat; } /** * Get Conditional Styles. Only used on supervisor. * * @return Conditional[] */ public function getConditionalStyles() { return $this->getActiveSheet()->getConditionalStyles($this->getActiveCell()); } /** * Set Conditional Styles. Only used on supervisor. * * @param Conditional[] $conditionalStyleArray Array of conditional styles * * @return $this */ public function setConditionalStyles(array $conditionalStyleArray) { $this->getActiveSheet()->setConditionalStyles($this->getSelectedCells(), $conditionalStyleArray); return $this; } /** * Get Protection. * * @return Protection */ public function getProtection() { return $this->protection; } /** * Get quote prefix. * * @return bool */ public function getQuotePrefix() { if ($this->isSupervisor) { return $this->getSharedComponent()->getQuotePrefix(); } return $this->quotePrefix; } /** * Set quote prefix. * * @param bool $quotePrefix * * @return $this */ public function setQuotePrefix($quotePrefix) { if ($quotePrefix == '') { $quotePrefix = false; } if ($this->isSupervisor) { $styleArray = ['quotePrefix' => $quotePrefix]; $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); } else { $this->quotePrefix = (bool) $quotePrefix; } return $this; } /** * Get hash code. * * @return string Hash code */ public function getHashCode() { return md5( $this->fill->getHashCode() . $this->font->getHashCode() . $this->borders->getHashCode() . $this->alignment->getHashCode() . $this->numberFormat->getHashCode() . $this->protection->getHashCode() . ($this->quotePrefix ? 't' : 'f') . __CLASS__ ); } /** * Get own index in style collection. * * @return int */ public function getIndex() { return $this->index; } /** * Set own index in style collection. * * @param int $index */ public function setIndex($index): void { $this->index = $index; } protected function exportArray1(): array { $exportedArray = []; $this->exportArray2($exportedArray, 'alignment', $this->getAlignment()); $this->exportArray2($exportedArray, 'borders', $this->getBorders()); $this->exportArray2($exportedArray, 'fill', $this->getFill()); $this->exportArray2($exportedArray, 'font', $this->getFont()); $this->exportArray2($exportedArray, 'numberFormat', $this->getNumberFormat()); $this->exportArray2($exportedArray, 'protection', $this->getProtection()); $this->exportArray2($exportedArray, 'quotePrefx', $this->getQuotePrefix()); return $exportedArray; } }