Minor corrections and new features

This commit is contained in:
Javier 2023-07-04 22:23:07 -03:00
parent 306aa7dad9
commit 5d14683e22
2 changed files with 345 additions and 150 deletions

View File

@ -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

View File

@ -1,17 +1,17 @@
<?php
namespace Shuchkin;
/** @noinspection ReturnTypeCanBeDeclaredInspection */
/** @noinspection PhpMissingReturnTypeInspection */
/** @noinspection NullCoalescingOperatorCanBeUsedInspection */
/** @noinspection PhpIssetCanBeReplacedWithCoalesceInspection */
namespace Shuchkin;
/**
* Class SimpleXLSXGen
* Export data to MS Excel. PHP XLSX generator
* Author: sergey.shuchkin@gmail.com
*
* Export data to MS Excel. PHP XLSX generator.
*
* @author Sergey Shuchkin <sergey.shuchkin@gmail.com>
*/
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' => '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/>
<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/>
</Relationships>',
'docProps/app.xml' => '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties">
<TotalTime>0</TotalTime>
<Application>{APP}</Application>
<Company>{COMPANY}</Company>
<Manager>{MANAGER}</Manager>
</Properties>',
'docProps/core.xml' => '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:dcmitype="http://purl.org/dc/dcmitype/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dcterms:created xsi:type="dcterms:W3CDTF">{DATE}</dcterms:created>
<dc:title>{TITLE}</dc:title>
<dc:subject>{SUBJECT}</dc:subject>
<dc:creator>{AUTHOR}</dc:creator>
<cp:lastModifiedBy>{LAST_MODIFY_BY}</cp:lastModifiedBy>
<cp:keywords>{KEYWORD}</cp:keywords>
<dc:description>{DESCRIPTION}</dc:description>
<cp:category>{CATEGORY}</cp:category>
<dc:language>en-US</dc:language>
<dcterms:modified xsi:type="dcterms:W3CDTF">{DATE}</dcterms:modified>
<cp:revision>1</cp:revision>
</cp:coreProperties>',
'xl/_rels/workbook.xml.rels' => '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
{RELS}
</Relationships>',
'xl/worksheets/sheet1.xml' => '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<dimension ref="{REF}"/>
{SHEETVIEWS}
{COLS}
<sheetData>{ROWS}</sheetData>
{AUTOFILTER}{MERGECELLS}{HYPERLINKS}
</worksheet>',
'xl/worksheets/_rels/sheet1.xml.rels' => '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">{HYPERLINKS}</Relationships>',
'xl/sharedStrings.xml' => '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="{CNT}" uniqueCount="{CNT}">{STRINGS}</sst>',
'xl/styles.xml' => '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
{NUMFMTS}
{FONTS}
{FILLS}
{BORDERS}
<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0" /></cellStyleXfs>
{XF}
<cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles>
</styleSheet>',
'xl/workbook.xml' => '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<fileVersion appName="{APP}"/>
<sheets>
{SHEETS}
</sheets>
</workbook>',
'[Content_Types].xml' => '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Override PartName="/rels/.rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
<Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/>
<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>
<Override PartName="/xl/_rels/workbook.xml.rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
<Override PartName="/xl/sharedStrings.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"/>
<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>
<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>
{TYPES}
</Types>',
'_rels/.rels' => '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' . self::NEWLINE .
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' . self::NEWLINE .
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>' . self::NEWLINE .
'<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/>' . self::NEWLINE .
'<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/>' . self::NEWLINE .
'</Relationships>',
'docProps/app.xml' => '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' . self::NEWLINE .
'<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties">' . self::NEWLINE .
'<TotalTime>0</TotalTime>' . self::NEWLINE .
'<Application>{APP}</Application>' . self::NEWLINE .
'<Company>{COMPANY}</Company>' . self::NEWLINE .
'<Manager>{MANAGER}</Manager>' . self::NEWLINE .
'</Properties>',
'docProps/core.xml' => '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' . self::NEWLINE .
'<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:dcmitype="http://purl.org/dc/dcmitype/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">' . self::NEWLINE .
'<dcterms:created xsi:type="dcterms:W3CDTF">{DATE}</dcterms:created>' . self::NEWLINE .
'<dc:title>{TITLE}</dc:title>' . self::NEWLINE .
'<dc:subject>{SUBJECT}</dc:subject>' . self::NEWLINE .
'<dc:creator>{AUTHOR}</dc:creator>' . self::NEWLINE .
'<cp:lastModifiedBy>{LAST_MODIFY_BY}</cp:lastModifiedBy>' . self::NEWLINE .
'<cp:keywords>{KEYWORD}</cp:keywords>' . self::NEWLINE .
'<dc:description>{DESCRIPTION}</dc:description>' . self::NEWLINE .
'<cp:category>{CATEGORY}</cp:category>' . self::NEWLINE .
'<dc:language>en-US</dc:language>' . self::NEWLINE .
'<dcterms:modified xsi:type="dcterms:W3CDTF">{DATE}</dcterms:modified>' . self::NEWLINE .
'<cp:revision>1</cp:revision>' . self::NEWLINE .
'</cp:coreProperties>',
'xl/_rels/workbook.xml.rels' => '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' . self::NEWLINE .
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' . self::NEWLINE .
'{RELS}' . self::NEWLINE .
'</Relationships>',
'xl/worksheets/sheet1.xml' => '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' . self::NEWLINE .
'<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">' . self::NEWLINE .
'<dimension ref="{REF}"/>' . self::NEWLINE .
'{SHEETVIEWS}' . self::NEWLINE .
'{COLS}' . self::NEWLINE .
'<sheetData>' . self::NEWLINE . '{ROWS}' . self::NEWLINE . '</sheetData>' . self::NEWLINE .
'{AUTOFILTER}' . '{MERGECELLS}' . self::NEWLINE . '{HYPERLINKS}' . self::NEWLINE .
'</worksheet>',
'xl/worksheets/_rels/sheet1.xml.rels' => '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' . self::NEWLINE .
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' . self::NEWLINE .
'{HYPERLINKS}' . self::NEWLINE .
'</Relationships>',
'xl/sharedStrings.xml' => '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' . self::NEWLINE .
'<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="{CNT}" uniqueCount="{CNT}">' . self::NEWLINE .
'{STRINGS}' . self::NEWLINE .
'</sst>',
'xl/styles.xml' => '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' . self::NEWLINE .
'<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">' . self::NEWLINE .
'{NUMFMTS}' . self::NEWLINE .
'{FONTS}' . self::NEWLINE .
'{FILLS}' . self::NEWLINE .
'{BORDERS}' . self::NEWLINE .
'<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0" /></cellStyleXfs>' . self::NEWLINE .
'{XF}' . self::NEWLINE .
'<cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles>' . self::NEWLINE .
'</styleSheet>',
'xl/workbook.xml' => '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' . self::NEWLINE .
'<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">' . self::NEWLINE .
'<fileVersion appName="{APP}"/>' . self::NEWLINE .
'<sheets>' . self::NEWLINE .
'{SHEETS}' . self::NEWLINE .
'</sheets>' . self::NEWLINE .
'</workbook>',
'[Content_Types].xml' => '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' . self::NEWLINE .
'<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">' . self::NEWLINE .
'<Override PartName="/rels/.rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>' . self::NEWLINE .
'<Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/>' . self::NEWLINE .
'<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>' . self::NEWLINE .
'<Override PartName="/xl/_rels/workbook.xml.rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>' . self::NEWLINE .
'<Override PartName="/xl/sharedStrings.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"/>' . self::NEWLINE .
'<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>' . self::NEWLINE .
'<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>' . self::NEWLINE .
'{TYPES}' . self::NEWLINE .
'</Types>',
];
// <col min="1" max="1" width="22.1796875" bestFit="1" customWidth="1"/>
// <row r="1" spans="1:2" x14ac:dyDescent="0.35"><c r="A1" t="s"><v>0</v></c><c r="B1"><v>100</v></c></row><row r="2" spans="1:2" x14ac:dyDescent="0.35"><c r="A2" t="s"><v>1</v></c><c r="B2"><v>200</v></c></row>
@ -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 .= '<Relationship Id="rId' . ($i + 1) . '" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"' .
' Target="worksheets/sheet' . ($i + 1) . ".xml\"/>\r\n";
' Target="worksheets/sheet' . ($i + 1) . '.xml"/>' . self::NEWLINE;
}
$s .= '<Relationship Id="rId' . ($i + 1) . '" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>' . "\r\n";
$s .= '<Relationship Id="rId' . ($i + 1) . '" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>' . self::NEWLINE;
$s .= '<Relationship Id="rId' . ($i + 2) . '" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings" Target="sharedStrings.xml"/>';
$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 .= '<sheet name="' . $this->esc($v['name']) . '" sheetId="' . ($k + 1) . '" r:id="rId' . ($k + 1) . '"/>';
}
$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 = '<si><t>' . implode("</t></si>\r\n<si><t>", $this->SI) . '</t></si>';
$template = str_replace(['{CNT}', '{STRINGS}'], [$si_cnt, $si], $template);
$template = str_replace(
['{CNT}', '{STRINGS}'],
[count($this->SI), '<si><t>' . implode('</t></si>' . self::NEWLINE . '<si><t>', $this->SI) . '</t></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[] = '<Relationship Id="' . $h['ID'] . '" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" Target="' . $this->esc($h['H']) . '" TargetMode="External"/>';
$RH[] = ' <Relationship Id="' . $h['ID'] . '" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" Target="' . $this->esc($h['H']) . '" TargetMode="External"/>';
}
}
$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[] = '<Override PartName="/xl/worksheets/_rels/sheet' . ($k + 1) . '.xml.rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>';
}
}
$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 = '<sheetViews><sheetView tabSelected="1" workbookViewId="0"'.($this->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 .= '<sheetViews>
<sheetView workbookViewId="0"' . ($this->rtl ? ' rightToLeft="1"' : '');
$SHEETVIEWS .= $PANE ? ">\r\n" . $PANE . "\r\n</sheetView>" : ' />';
$SHEETVIEWS .= "\r\n</sheetViews>";
$SHEETVIEWS .= '<sheetViews>' . self::NEWLINE . '<sheetView workbookViewId="0"' . ($this->rtl ? ' rightToLeft="1"' : '');
$SHEETVIEWS .= $PANE ? ('>' . self::NEWLINE . $PANE . self::NEWLINE . '</sheetView>') : ' />';
$SHEETVIEWS .= self::NEWLINE . '</sheetViews>';
}
$COLS[] = '<cols>';
$CUR_ROW = 0;
@ -701,7 +709,7 @@ class SimpleXLSXGen
}
$cname = $this->num2name($CUR_COL) . $CUR_ROW;
if ($v === null || $v === '') {
$row .= '<c r="' . $cname . '"/>';
$row .= ' <c r="' . $cname . '"/>';
continue;
}
$ct = $cv = $cf = null;
@ -918,15 +926,15 @@ class SimpleXLSXGen
$this->XF[] = [$N, $A, $F, $FL, $C, $BG, $BR, $FS];
}
}
$row .= '<c r="' . $cname . '"' . ($ct ? ' t="' . $ct . '"' : '') . ($cs ? ' s="' . $cs . '"' : '') . '>'
. ($cf ? '<f>' . $cf . '</f>' : '')
. ($ct === 'inlineStr' ? '<is><t>' . $cv . '</t></is>' : '<v>' . $cv . '</v>') . "</c>\r\n";
$row .= ' <c r="' . $cname . '"' . ($ct ? ' t="' . $ct . '"' : '') . ($cs ? ' s="' . $cs . '"' : '') . '>'
. ($cf ? '<f>' . $cf . '</f>' : '')
. ($ct === 'inlineStr' ? '<is><t>' . $cv . '</t></is>' : '<v>' . $cv . '</v>') . '</c>' . self::NEWLINE;
}
$ROWS[] = '<row r="' . $CUR_ROW . '"' . ($RH ? ' customHeight="1" ht="' . $RH . '"' : '') . '>' . $row . "</row>";
$ROWS[] = ' <row r="' . $CUR_ROW . '"' . ($RH ? ' customHeight="1" ht="' . $RH . '"' : '') . '>' . self::NEWLINE . $row . ' </row>';
}
foreach ($COL as $k => $max) {
$w = isset($this->sheets[$idx]['colwidth'][$k]) ? $this->sheets[$idx]['colwidth'][$k] : min($max + 1, 60);
$COLS[] = '<col min="' . $k . '" max="' . $k . '" width="' . $w . '" customWidth="1" />';
$COLS[] = ' <col min="' . $k . '" max="' . $k . '" width="' . $w . '" customWidth="1" />';
}
$COLS[] = '</cols>';
$REF = 'A1:' . $this->num2name(count($COL)) . $CUR_ROW;
@ -937,15 +945,15 @@ class SimpleXLSXGen
$AUTOFILTER = '';
if ($this->sheets[$idx]['autofilter']) {
$AUTOFILTER = '<autoFilter ref="' . $this->sheets[$idx]['autofilter'] . '" />';
$AUTOFILTER = '<autoFilter ref="' . $this->sheets[$idx]['autofilter'] . '"/>' . self::NEWLINE;
}
$MERGECELLS = [];
if (count($this->sheets[$idx]['mergecells'])) {
$MERGECELLS[] = '';
//$MERGECELLS[] = '';
$MERGECELLS[] = '<mergeCells count="' . count($this->sheets[$idx]['mergecells']) . '">';
foreach ($this->sheets[$idx]['mergecells'] as $m) {
$MERGECELLS[] = '<mergeCell ref="' . $m . '"/>';
$MERGECELLS[] = ' <mergeCell ref="' . $m . '"/>';
}
$MERGECELLS[] = '</mergeCells>';
}
@ -954,7 +962,7 @@ class SimpleXLSXGen
if (count($this->sheets[$idx]['hyperlinks'])) {
$HYPERLINKS[] = '<hyperlinks>';
foreach ($this->sheets[$idx]['hyperlinks'] as $h) {
$HYPERLINKS[] = '<hyperlink ref="' . $h['R'] . '"' . ($h['ID'] ? ' r:id="' . $h['ID'] . '"' : '') . ' location="' . $this->esc($h['L']) . '" display="' . $this->esc($h['H'] . ($h['L'] ? ' - ' . $h['L'] : '')) . '" />';
$HYPERLINKS[] = ' <hyperlink ref="' . $h['R'] . '"' . ($h['ID'] ? ' r:id="' . $h['ID'] . '"' : '') . ' location="' . $this->esc($h['L']) . '" display="' . $this->esc($h['H'] . ($h['L'] ? ' - ' . $h['L'] : '')) . '"/>';
}
$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;
}
}