From 5d14683e2250f51aad2034189443fd200246d1e6 Mon Sep 17 00:00:00 2001 From: Javier Date: Tue, 4 Jul 2023 22:23:07 -0300 Subject: [PATCH] Minor corrections and new features --- README.md | 3 +- src/SimpleXLSXGen.php | 492 +++++++++++++++++++++++++++++------------- 2 files changed, 345 insertions(+), 150 deletions(-) diff --git a/README.md b/README.md index 3a27a35..a43575c 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,8 @@ exit(); $xlsx->autoFilter('A1:B10'); // Freeze rows and columns from top-left corner up to, but not including, -// the row and column of the indicated cell +// the row and column of the indicated cell (in A1 format as string or +// R1C1 format as 2-element integer array) $xlsx->freezePanes('C3'); // RTL mode diff --git a/src/SimpleXLSXGen.php b/src/SimpleXLSXGen.php index 89c2b8f..1fb59fe 100644 --- a/src/SimpleXLSXGen.php +++ b/src/SimpleXLSXGen.php @@ -1,17 +1,17 @@ */ class SimpleXLSXGen { @@ -40,6 +40,7 @@ class SimpleXLSXGen protected $keywords; protected $category; protected $lastModifiedBy; + const N_NORMAL = 0; // General const N_INT = 1; // 0 const N_DEC = 2; // 0.00 @@ -89,6 +90,8 @@ class SimpleXLSXGen const B_MEDIUM_DASH_DOT_DOT = 12; const B_SLANT_DASH_DOT = 13; + const NEWLINE = "\r\n"; + public function __construct() { $this->subject = ''; @@ -142,78 +145,83 @@ class SimpleXLSXGen ]; $this->XF_KEYS[implode('-', $this->XF[0])] = 0; // & keys $this->XF_KEYS[implode('-', $this->XF[1])] = 1; + $this->template = [ - '_rels/.rels' => ' - - - - -', - 'docProps/app.xml' => ' - -0 -{APP} -{COMPANY} -{MANAGER} -', - 'docProps/core.xml' => ' - -{DATE} - {TITLE} - {SUBJECT} - {AUTHOR} - {LAST_MODIFY_BY} - {KEYWORD} - {DESCRIPTION} - {CATEGORY} - en-US -{DATE} -1 -', - 'xl/_rels/workbook.xml.rels' => ' - -{RELS} -', - 'xl/worksheets/sheet1.xml' => ' - - -{SHEETVIEWS} -{COLS} -{ROWS} -{AUTOFILTER}{MERGECELLS}{HYPERLINKS} -', - 'xl/worksheets/_rels/sheet1.xml.rels' => ' -{HYPERLINKS}', - 'xl/sharedStrings.xml' => ' -{STRINGS}', - 'xl/styles.xml' => ' - -{NUMFMTS} -{FONTS} -{FILLS} -{BORDERS} - -{XF} - -', - 'xl/workbook.xml' => ' - - - -{SHEETS} - -', - '[Content_Types].xml' => ' - - - - - - - - -{TYPES} -', + '_rels/.rels' => '' . self::NEWLINE . + '' . self::NEWLINE . + '' . self::NEWLINE . + '' . self::NEWLINE . + '' . self::NEWLINE . + '', + 'docProps/app.xml' => '' . self::NEWLINE . + '' . self::NEWLINE . + '0' . self::NEWLINE . + '{APP}' . self::NEWLINE . + '{COMPANY}' . self::NEWLINE . + '{MANAGER}' . self::NEWLINE . + '', + 'docProps/core.xml' => '' . self::NEWLINE . + '' . self::NEWLINE . + '{DATE}' . self::NEWLINE . + '{TITLE}' . self::NEWLINE . + '{SUBJECT}' . self::NEWLINE . + '{AUTHOR}' . self::NEWLINE . + '{LAST_MODIFY_BY}' . self::NEWLINE . + '{KEYWORD}' . self::NEWLINE . + '{DESCRIPTION}' . self::NEWLINE . + '{CATEGORY}' . self::NEWLINE . + 'en-US' . self::NEWLINE . + '{DATE}' . self::NEWLINE . + '1' . self::NEWLINE . + '', + 'xl/_rels/workbook.xml.rels' => '' . self::NEWLINE . + '' . self::NEWLINE . + '{RELS}' . self::NEWLINE . + '', + 'xl/worksheets/sheet1.xml' => '' . self::NEWLINE . + '' . self::NEWLINE . + '' . self::NEWLINE . + '{SHEETVIEWS}' . self::NEWLINE . + '{COLS}' . self::NEWLINE . + '' . self::NEWLINE . '{ROWS}' . self::NEWLINE . '' . self::NEWLINE . + '{AUTOFILTER}' . '{MERGECELLS}' . self::NEWLINE . '{HYPERLINKS}' . self::NEWLINE . + '', + 'xl/worksheets/_rels/sheet1.xml.rels' => '' . self::NEWLINE . + '' . self::NEWLINE . + '{HYPERLINKS}' . self::NEWLINE . + '', + 'xl/sharedStrings.xml' => '' . self::NEWLINE . + '' . self::NEWLINE . + '{STRINGS}' . self::NEWLINE . + '', + 'xl/styles.xml' => '' . self::NEWLINE . + '' . self::NEWLINE . + '{NUMFMTS}' . self::NEWLINE . + '{FONTS}' . self::NEWLINE . + '{FILLS}' . self::NEWLINE . + '{BORDERS}' . self::NEWLINE . + '' . self::NEWLINE . + '{XF}' . self::NEWLINE . + '' . self::NEWLINE . + '', + 'xl/workbook.xml' => '' . self::NEWLINE . + '' . self::NEWLINE . + '' . self::NEWLINE . + '' . self::NEWLINE . + '{SHEETS}' . self::NEWLINE . + '' . self::NEWLINE . + '', + '[Content_Types].xml' => '' . self::NEWLINE . + '' . self::NEWLINE . + '' . self::NEWLINE . + '' . self::NEWLINE . + '' . self::NEWLINE . + '' . self::NEWLINE . + '' . self::NEWLINE . + '' . self::NEWLINE . + '' . self::NEWLINE . + '{TYPES}' . self::NEWLINE . + '', ]; // // 01001200 @@ -311,7 +319,8 @@ class SimpleXLSXGen ob_end_clean(); } fseek($fh, 0); - fpassthru($fh); + if (function_exists('fpassthru')) fpassthru($fh); + else echo stream_get_contents($fh); fclose($fh); return true; } @@ -331,11 +340,10 @@ class SimpleXLSXGen $s = ''; for ($i = 0; $i < $cnt_sheets; $i++) { $s .= '\r\n"; + ' Target="worksheets/sheet' . ($i + 1) . '.xml"/>' . self::NEWLINE; } - $s .= '' . "\r\n"; + $s .= '' . self::NEWLINE; $s .= ''; - $template = str_replace('{RELS}', $s, $template); $this->_writeEntry($fh, $cdrec, $cfilename, $template); $entries++; @@ -344,30 +352,38 @@ class SimpleXLSXGen foreach ($this->sheets as $k => $v) { $s .= ''; } - $search = ['{SHEETS}', '{APP}']; - $replace = [$s, $this->esc($this->application)]; - $template = str_replace($search, $replace, $template); + $template = str_replace( + ['{SHEETS}', '{APP}'], + [$s, $this->esc($this->application)], + $template + ); $this->_writeEntry($fh, $cdrec, $cfilename, $template); $entries++; } elseif ($cfilename === 'docProps/app.xml') { - $search = ['{APP}', '{COMPANY}', '{MANAGER}']; - $replace = [$this->esc($this->application), $this->esc($this->company), $this->esc($this->manager)]; - $template = str_replace($search, $replace, $template); + $template = str_replace( + ['{APP}', '{COMPANY}', '{MANAGER}'], + [$this->esc($this->application), $this->esc($this->company), $this->esc($this->manager)], + $template + ); $this->_writeEntry($fh, $cdrec, $cfilename, $template); $entries++; } elseif ($cfilename === 'docProps/core.xml') { - $search = ['{DATE}', '{AUTHOR}', '{TITLE}', '{SUBJECT}', '{KEYWORD}', '{DESCRIPTION}', '{CATEGORY}', '{LAST_MODIFY_BY}']; - $replace = [gmdate('Y-m-d\TH:i:s\Z'), $this->esc($this->author), $this->esc($this->title), $this->esc($this->subject), $this->esc($this->keywords), $this->esc($this->description), $this->esc($this->category), $this->esc($this->lastModifiedBy)]; - $template = str_replace($search, $replace, $template); + $template = str_replace( + ['{DATE}', '{AUTHOR}', '{TITLE}', '{SUBJECT}', '{KEYWORD}', '{DESCRIPTION}', '{CATEGORY}', '{LAST_MODIFY_BY}'], + [gmdate('Y-m-d\TH:i:s\Z'), $this->esc($this->author), $this->esc($this->title), $this->esc($this->subject), $this->esc($this->keywords), $this->esc($this->description), $this->esc($this->category), $this->esc($this->lastModifiedBy)], + $template + ); $this->_writeEntry($fh, $cdrec, $cfilename, $template); $entries++; } elseif ($cfilename === 'xl/sharedStrings.xml') { if (!count($this->SI)) { $this->SI[] = 'No Data'; } - $si_cnt = count($this->SI); - $si = '' . implode("\r\n", $this->SI) . ''; - $template = str_replace(['{CNT}', '{STRINGS}'], [$si_cnt, $si], $template); + $template = str_replace( + ['{CNT}', '{STRINGS}'], + [count($this->SI), '' . implode('' . self::NEWLINE . '', $this->SI) . ''], + $template + ); $this->_writeEntry($fh, $cdrec, $cfilename, $template); $entries++; } elseif ($cfilename === 'xl/worksheets/sheet1.xml') { @@ -385,10 +401,10 @@ class SimpleXLSXGen $filename = 'xl/worksheets/_rels/sheet' . ($k + 1) . '.xml.rels'; foreach ($v['hyperlinks'] as $h) { if ($h['ID']) { - $RH[] = ''; + $RH[] = ' '; } } - $xml = str_replace('{HYPERLINKS}', implode("\r\n", $RH), $template); + $xml = str_replace('{HYPERLINKS}', implode(self::NEWLINE, $RH), $template); $this->_writeEntry($fh, $cdrec, $filename, $xml); $entries++; } @@ -402,7 +418,7 @@ class SimpleXLSXGen $TYPES[] = ''; } } - $template = str_replace('{TYPES}', implode("\r\n", $TYPES), $template); + $template = str_replace('{TYPES}', implode(self::NEWLINE, $TYPES), $template); $this->_writeEntry($fh, $cdrec, $cfilename, $template); $entries++; } elseif ($cfilename === 'xl/styles.xml') { @@ -465,7 +481,6 @@ class SimpleXLSXGen if ($xf[1] & self::A_WRAPTEXT) { $align .= ' wrapText="1"'; } - // border $BR_ID = 0; if ($xf[6] !== '') { @@ -483,7 +498,7 @@ class SimpleXLSXGen $ba[] = $ba[0]; } if (!isset($ba[4])) { // diagonal - $ba[] = 'none'; + $ba[] = 'none'; } $sides = ['left' => 3, 'right' => 1, 'top' => 0, 'bottom' => 2, 'diagonal' => 4]; foreach ($sides as $side => $idx) { @@ -534,7 +549,7 @@ class SimpleXLSXGen $template = str_replace( ['{NUMFMTS}', '{FONTS}', '{XF}', '{FILLS}', '{BORDERS}'], - [implode("\r\n", $NF), implode("\r\n", $FONTS), implode("\r\n", $XF), implode("\r\n", $FILLS), implode("\r\n", $BR)], + [implode(self::NEWLINE, $NF), implode(self::NEWLINE, $FONTS), implode(self::NEWLINE, $XF), implode(self::NEWLINE, $FILLS), implode(self::NEWLINE, $BR)], $template ); $this->_writeEntry($fh, $cdrec, $cfilename, $template); @@ -556,7 +571,6 @@ class SimpleXLSXGen fwrite($fh, pack('V', $before_cd)); // offset to start of central dir fwrite($fh, pack('v', mb_strlen($zipComments, '8bit'))); // .zip file comment length fwrite($fh, $zipComments); - return true; } @@ -642,24 +656,19 @@ class SimpleXLSXGen setlocale(LC_NUMERIC, 'C'); $COLS = []; $ROWS = []; - // $SHEETVIEWS = 'rtl ? ' rightToLeft="1"' : '').'>'; $SHEETVIEWS = ''; $PANE = ''; if (count($this->sheets[$idx]['rows'])) { if ($this->sheets[$idx]['frozen'] !== '' || isset($this->sheets[$idx]['frozen'][0]) || isset($this->sheets[$idx]['frozen'][1])) { - // $AC = 'A1'; // Active Cell - $x = $y = 0; - if (is_string($this->sheets[$idx]['frozen'])) { + if (is_string($this->sheets[$idx]['frozen'])) { //A1 format -> store ($AC) and convert to R1C1 0-based ($y/$x) $AC = $this->sheets[$idx]['frozen']; - self::cell2coord($AC, $x, $y); - } else { - if (isset($this->sheets[$idx]['frozen'][0])) { - $x = $this->sheets[$idx]['frozen'][0]; - } - if (isset($this->sheets[$idx]['frozen'][1])) { - $y = $this->sheets[$idx]['frozen'][1]; - } - $AC = self::coord2cell($x, $y); + [$y, $x] = self::cell2coord($AC); + $y--; + $x--; + } else { //R1C1 1-based format -> store in R1C1 0-based ($y/$x) and convert to A1 format ($AC) + $y = $this->sheets[$idx]['frozen'][0] - 1; + $x = $this->sheets[$idx]['frozen'][1] - 1; + $AC = self::coord2cell($this->sheets[$idx]['frozen']); } if ($x > 0 || $y > 0) { $split = ''; @@ -681,10 +690,9 @@ class SimpleXLSXGen } } if ($this->rtl || $PANE) { - $SHEETVIEWS .= ' -rtl ? ' rightToLeft="1"' : ''); - $SHEETVIEWS .= $PANE ? ">\r\n" . $PANE . "\r\n" : ' />'; - $SHEETVIEWS .= "\r\n"; + $SHEETVIEWS .= '' . self::NEWLINE . 'rtl ? ' rightToLeft="1"' : ''); + $SHEETVIEWS .= $PANE ? ('>' . self::NEWLINE . $PANE . self::NEWLINE . '') : ' />'; + $SHEETVIEWS .= self::NEWLINE . ''; } $COLS[] = ''; $CUR_ROW = 0; @@ -701,7 +709,7 @@ class SimpleXLSXGen } $cname = $this->num2name($CUR_COL) . $CUR_ROW; if ($v === null || $v === '') { - $row .= ''; + $row .= ' '; continue; } $ct = $cv = $cf = null; @@ -918,15 +926,15 @@ class SimpleXLSXGen $this->XF[] = [$N, $A, $F, $FL, $C, $BG, $BR, $FS]; } } - $row .= '' - . ($cf ? '' . $cf . '' : '') - . ($ct === 'inlineStr' ? '' . $cv . '' : '' . $cv . '') . "\r\n"; + $row .= ' ' + . ($cf ? '' . $cf . '' : '') + . ($ct === 'inlineStr' ? '' . $cv . '' : '' . $cv . '') . '' . self::NEWLINE; } - $ROWS[] = '' . $row . ""; + $ROWS[] = ' ' . self::NEWLINE . $row . ' '; } foreach ($COL as $k => $max) { $w = isset($this->sheets[$idx]['colwidth'][$k]) ? $this->sheets[$idx]['colwidth'][$k] : min($max + 1, 60); - $COLS[] = ''; + $COLS[] = ' '; } $COLS[] = ''; $REF = 'A1:' . $this->num2name(count($COL)) . $CUR_ROW; @@ -937,15 +945,15 @@ class SimpleXLSXGen $AUTOFILTER = ''; if ($this->sheets[$idx]['autofilter']) { - $AUTOFILTER = ''; + $AUTOFILTER = '' . self::NEWLINE; } $MERGECELLS = []; if (count($this->sheets[$idx]['mergecells'])) { - $MERGECELLS[] = ''; + //$MERGECELLS[] = ''; $MERGECELLS[] = ''; foreach ($this->sheets[$idx]['mergecells'] as $m) { - $MERGECELLS[] = ''; + $MERGECELLS[] = ' '; } $MERGECELLS[] = ''; } @@ -954,7 +962,7 @@ class SimpleXLSXGen if (count($this->sheets[$idx]['hyperlinks'])) { $HYPERLINKS[] = ''; foreach ($this->sheets[$idx]['hyperlinks'] as $h) { - $HYPERLINKS[] = ''; + $HYPERLINKS[] = ' '; } $HYPERLINKS[] = ''; } @@ -966,17 +974,23 @@ class SimpleXLSXGen ['{REF}', '{COLS}', '{ROWS}', '{AUTOFILTER}', '{MERGECELLS}', '{HYPERLINKS}', '{SHEETVIEWS}'], [ $REF, - implode("\r\n", $COLS), - implode("\r\n", $ROWS), + implode(self::NEWLINE, $COLS), + implode(self::NEWLINE, $ROWS), $AUTOFILTER, - implode("\r\n", $MERGECELLS), - implode("\r\n", $HYPERLINKS), + implode(self::NEWLINE, $MERGECELLS), + implode(self::NEWLINE, $HYPERLINKS), $SHEETVIEWS ], $template ); } + /** + * Convert column number (1, 2, ...) to column letter (A, B, ..., Z, AA, ...) + * + * @param integer $num Column number + * @return string Column letter(s) + */ public function num2name($num) { $numeric = ($num - 1) % 26; @@ -995,9 +1009,9 @@ class SimpleXLSXGen return $excelTime; } // self::CALENDAR_WINDOWS_1900 - $excel1900isLeapYear = True; + $excel1900isLeapYear = true; if (($year === 1900) && ($month <= 2)) { - $excel1900isLeapYear = False; + $excel1900isLeapYear = false; } $myExcelBaseDate = 2415020; // Julian base date Adjustment @@ -1019,13 +1033,11 @@ class SimpleXLSXGen $this->defaultFont = $name; return $this; } - public function setDefaultFontSize($size) { $this->defaultFontSize = $size; return $this; } - public function setTitle($title) { $this->title = $title; @@ -1066,7 +1078,6 @@ class SimpleXLSXGen $this->category = $category; return $this; } - public function setApplication($application) { $this->application = $application; @@ -1083,18 +1094,17 @@ class SimpleXLSXGen $this->sheets[$this->curSheet]['autofilter'] = $range; return $this; } - public function mergeCells($range) { $this->sheets[$this->curSheet]['mergecells'][] = $range; return $this; } - public function setColWidth($col, $width) { $this->sheets[$this->curSheet]['colwidth'][$col] = $width; return $this; } + public function rightToLeft($value = true) { $this->rtl = $value; @@ -1122,19 +1132,31 @@ class SimpleXLSXGen return $id; } - public static function raw($value) { return "\0" . $value; } - public static function cell2coord($cell, &$x, &$y) + public function freezePanes($cell) + { + $this->sheets[$this->curSheet]['frozen'] = $cell; + return $this; + } + + /** + * Convert A1 cell reference style to R1C1 cell reference style (row/col number starting from 1) + * + * @param string $cell Cell reference in A1 format + * + * @return array Cell reference in R1C1 format as a two element array [row (ie. y coord), col (ie. x coord)] + */ + public static function cell2coord($cell) { $x = $y = 0; $lettercount = 0; - $cell = str_replace([' ', '\t', '\r', '\n', '\v', '\0'], '', $cell); + $cell = str_replace([' ', '\t', '\r', '\n', '\v', '\0', '$'], '', $cell); if (empty($cell)) { - return; + return []; } $cell = strtoupper($cell); for ($i = 0, $len = strlen($cell); $i < $len; $i++) { @@ -1149,24 +1171,196 @@ class SimpleXLSXGen $x += (ord($cell[$i]) - ord('A') + 1) * (26 ** $e); $e++; } + $x++; //to make 1-based } if ($lettercount < strlen($cell)) { - $y = ((int)substr($cell, $lettercount)) - 1; + $y = ((int)substr($cell, $lettercount)); } + return [$y, $x]; } - public static function coord2cell($x, $y) + /** + * Convert R1C1 cell reference style (row/col 1-based) to A1 cell reference style + * + * @param integer $y Row number (starting from 1) or a 2 element array with reference. + * @param integer $x Optional. Column number (starting from 1). Not used if $y is an array. + * + * @return string Cell reference in A1 format + */ + public static function coord2cell($y, $x = null) { + if (is_array($y)) { + $x = $y[1]; + $y = $y[0]; + } $c = ''; - for ($i = $x; $i >= 0; $i = ((int)($i / 26)) - 1) { + for ($i = $x - 1; $i >= 0; $i = ((int)($i / 26)) - 1) { $c = chr(ord('A') + $i % 26) . $c; } - return $c . ($y + 1); + return $c . strval($y); } - public function freezePanes($cell) + /** + * Convert A1 range reference style to R1C1 cell reference style + * + * @param string $range Range reference in A1 format + * + * @return array Cell reference in R1C1 format as a four element array [top-left row, top-left col, bottom-right row, bottom-right col] + */ + public static function range2coord($range) { - $this->sheets[$this->curSheet]['frozen'] = $cell; - return $this; + $temp = explode(':', $range); + if (empty($temp[1])) return self::cell2coord($temp[0]); + return array_merge(self::cell2coord($temp[0]), self::cell2coord($temp[1])); } -} + + /** + * Convert R1C1 range reference style (row/col 1-based) to A1 range reference style + * + * Possible parameters: + * top-left row number, top-left column number, bottom-right row number, bottom-right column number + * [top-left row number, top-left column number], [bottom-right row number, bottom-right column number] + * [top-left row number, top-left column number, bottom-right row number, bottom-right column number] + * + * @return string Range reference in A1 format + */ + public static function coord2range($p1, ...$p) + { + switch (count($p)) { + case 0: $yi = $p1[0]; $xi = $p1[1]; $yf = $p1[2]; $xf = $p1[3]; break; + case 1: $yi = $p1[0]; $xi = $p1[1]; $yf = $p[0][0]; $xf = $p[0][1]; break; + default: $yi = $p1; $xi = $p[0]; $yf = $p[1]; $xf = $p[2]; break; + } + return self::coord2cell($yi, $xi) . ':' . self::coord2cell($yf, $xf); + } + + /** + * Change or add cell style parameters without losing the original content or previously established styles + * + * @param string $data Data matrix (2-dim 0-based array) where style will be modified. Passed by reference. + * @param string|array $cell Cell to modify in A1 format (string) or R1C1 format (2 element array) + * @param array $style Associative array with style attributes name/value pairs to change/add + * @param integer $edge Optional. If border attribute specified in $style, this parameter indicates on which edge is applied. + * The unspecified edges retain their original value (or 'none' if not specified). + * Default: all edges. You can combine (adding or oring) the constants A_TOP, A_RIGHT, A_BOTTOM, A_LEFT + * @return void + */ + public static function setCellStyle(&$data, $cell, $style, $edge = self::A_DEFAULT) + { + if (is_string($cell)) $cell = self::cell2coord($cell);//A1 to R1C1 if needed + if (!array_key_exists($cell[0] - 1, $data) || !array_key_exists($cell[1] - 1, $data[$cell[0] - 1])) return;//quit if cell doesn't exist + if ($edge === self::A_DEFAULT) $edge = self::A_TOP + self::A_RIGHT + self::A_BOTTOM + self::A_LEFT; + //border processing + if (array_key_exists('border', $style) && $edge !== (self::A_TOP + self::A_RIGHT + self::A_BOTTOM + self::A_LEFT)) { + //get original border as 4 element array + $oldBorder = self::getTagAttributes($data[$cell[0] - 1][$cell[1] - 1], 'style', 'border'); + if (empty($oldBorder)) { + $oldBorder = ['none', 'none', 'none', 'none']; //if border not present in original cell, create a no-border attribute + } else { + $oldBorder = explode(' ', $oldBorder); + if (count($oldBorder) == 1) $oldBorder = [$oldBorder[0], $oldBorder[0], $oldBorder[0], $oldBorder[0]]; + } + //get desired border as 4 element array + $border = explode(' ', $style['border']); + if (count($border) == 1) $border = [$border[0], $border[0], $border[0], $border[0]]; + //new border copied from old one + $newBorder = $oldBorder; + //set border following $edge + if ($edge & self::A_TOP) $newBorder[0] = $border[0]; + if ($edge & self::A_RIGHT) $newBorder[1] = $border[1]; + if ($edge & self::A_BOTTOM) $newBorder[2] = $border[2]; + if ($edge & self::A_LEFT) $newBorder[3] = $border[3]; + //implode to the new border + $style['border'] = implode(' ', $newBorder); + } + $data[$cell[0] - 1][$cell[1] - 1] = self::setTagAttributes($data[$cell[0] - 1][$cell[1] - 1], 'style', $style); + } + + /** + * Apply style to a sheet range. Border style are only applied to edges of range. + * + * @param array $data Data matrix (2-dim 0-based array) where style will be modified. Passed by reference. + * @param string|array $range Range to modify in A1 format (string) or R1C1 format (4 element array) + * @param array $style Associative array with style attributes name/value pairs to change + * + * @return void + */ + public static function setRangeStyle(&$data, $range, $style) + { + if (is_string($range)) $range = self::range2coord($range); + // 1 cell range + if (!isset($range[2])) return self::setCellStyle($data, $range, $style); //1 cell case + // Edge of range + self::setCellStyle($data, [$range[0], $range[1]], $style, self::A_TOP + self::A_LEFT); //top-left corner + for ($i = $range[1] + 1; $i < $range[3]; $i++) self::setCellStyle($data, [$range[0], $i], $style, self::A_TOP); //loop top edge (except first and last column) + self::setCellStyle($data, [$range[0], $range[3]], $style, self::A_TOP + self::A_RIGHT); //top-right corner + for ($i = $range[0] + 1; $i < $range[2]; $i++) self::setCellStyle($data, [$i, $range[3]], $style, self::A_RIGHT); //loop right edge (except top and bottom row) + self::setCellStyle($data, [$range[2], $range[3]], $style, self::A_BOTTOM + self::A_RIGHT); //bottom-right corner + for ($i = $range[3] - 1; $i > $range[1]; $i--) self::setCellStyle($data, [$range[2], $i], $style, self::A_BOTTOM); //loop bottom edge (except right and left column) + self::setCellStyle($data, [$range[2], $range[1]], $style, self::A_BOTTOM + self::A_LEFT); //bottom-left corner + for ($i = $range[2] - 1; $i > $range[0]; $i--) self::setCellStyle($data, [$i, $range[1]], $style, self::A_LEFT); //loop left edge (except bottom and top row) + // Internal cells + if (array_key_exists('border', $style)) unset($style['border']); + for ($i = $range[0] + 1; $i < $range[2]; $i++) { + for ($j = $range[1] + 1; $j < $range[3]; $j++) { + self::setCellStyle($data, [$i, $j], $style); + } + } + } + + /** + * Get tag attributes from a HTML-style tag as an associative array with attributes name/value pairs. + * If $attribute specified, get value of the specified attribute. + * + * @param string $str Text to extract tag attributes + * @param string $tag The tag to look for + * @param string $attribute Optional. Get the value of specified attribute. Default: get all attributes name/value as an array. + + * @return array|string Attributes name/value as an array ($attribute == '') or attribute value as a string ($attribute != ''). + */ + public static function getTagAttributes($str, $tag, $attribute = '') + { + $attributes = ($attribute === '') ? [] : ''; + if (preg_match("/<{$tag}\s+([^>]+)(?:>)/i", $str, $m)) { + $tagcontent = $m[1]; + if (preg_match_all('/([a-z0-9\-_]+)=(["\'])(.*?)\2/si', $tagcontent, $m, PREG_SET_ORDER)) { + foreach ($m as $match) { + if ($attribute !== '') { + if ($attribute === $match[1]) return $match[3]; + } else { + $attributes[$match[1]] = $match[3]; + } + } + } + } + return $attributes; + } + + /** + * Change HTML-style tag attributes. If tag doesn't exist, is created surrounding the previous content. + * + * @param string $str Text to modify + * @param string $tag Tag in text to modify + * @param array $attributes Associative array with attribute name/value pairs + * + * @return void + */ + public static function setTagAttributes($str, $tag, $attributes) + { + $opening = "<{$tag}"; + if (preg_match("/(<{$tag}\s*[^>]*)(>.*<\/{$tag}>)/i", $str, $m)) { + $stropen = $m[1]; + } else { + $str = "<{$tag}>{$str}"; + $stropen = $opening; + } + //merge original attributes and specified attributes (replacing if necessary) + $attributes = array_merge(self::getTagAttributes($str, $tag), $attributes); + //add attributes to tag + foreach ($attributes as $attr => $value) $opening .= " {$attr}=\"{$value}\""; + //replace original tag (with attributes) with the new one + $str = substr_replace($str, $opening, strpos($str, $stropen), strlen($stropen)); + return $str; + } + +} \ No newline at end of file