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 .
+ '',
];
//
// 0100
1200
@@ -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}{$tag}>";
+ $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