1
0
mirror of https://github.com/shuchkin/simplexlsxgen.git synced 2023-08-10 21:12:59 +03:00

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'); $xlsx->autoFilter('A1:B10');
// Freeze rows and columns from top-left corner up to, but not including, // 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'); $xlsx->freezePanes('C3');
// RTL mode // RTL mode

View File

@ -1,17 +1,17 @@
<?php <?php
namespace Shuchkin;
/** @noinspection ReturnTypeCanBeDeclaredInspection */ /** @noinspection ReturnTypeCanBeDeclaredInspection */
/** @noinspection PhpMissingReturnTypeInspection */ /** @noinspection PhpMissingReturnTypeInspection */
/** @noinspection NullCoalescingOperatorCanBeUsedInspection */ /** @noinspection NullCoalescingOperatorCanBeUsedInspection */
/** @noinspection PhpIssetCanBeReplacedWithCoalesceInspection */ /** @noinspection PhpIssetCanBeReplacedWithCoalesceInspection */
namespace Shuchkin;
/** /**
* Class SimpleXLSXGen * 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 class SimpleXLSXGen
{ {
@ -40,6 +40,7 @@ class SimpleXLSXGen
protected $keywords; protected $keywords;
protected $category; protected $category;
protected $lastModifiedBy; protected $lastModifiedBy;
const N_NORMAL = 0; // General const N_NORMAL = 0; // General
const N_INT = 1; // 0 const N_INT = 1; // 0
const N_DEC = 2; // 0.00 const N_DEC = 2; // 0.00
@ -89,6 +90,8 @@ class SimpleXLSXGen
const B_MEDIUM_DASH_DOT_DOT = 12; const B_MEDIUM_DASH_DOT_DOT = 12;
const B_SLANT_DASH_DOT = 13; const B_SLANT_DASH_DOT = 13;
const NEWLINE = "\r\n";
public function __construct() public function __construct()
{ {
$this->subject = ''; $this->subject = '';
@ -142,78 +145,83 @@ class SimpleXLSXGen
]; ];
$this->XF_KEYS[implode('-', $this->XF[0])] = 0; // & keys $this->XF_KEYS[implode('-', $this->XF[0])] = 0; // & keys
$this->XF_KEYS[implode('-', $this->XF[1])] = 1; $this->XF_KEYS[implode('-', $this->XF[1])] = 1;
$this->template = [ $this->template = [
'_rels/.rels' => '<?xml version="1.0" encoding="UTF-8" standalone="yes"?> '_rels/.rels' => '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' . self::NEWLINE .
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"> '<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"/> '<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"/> '<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"/> '<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/>' . self::NEWLINE .
</Relationships>', '</Relationships>',
'docProps/app.xml' => '<?xml version="1.0" encoding="UTF-8" standalone="yes"?> 'docProps/app.xml' => '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' . self::NEWLINE .
<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"> '<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties">' . self::NEWLINE .
<TotalTime>0</TotalTime> '<TotalTime>0</TotalTime>' . self::NEWLINE .
<Application>{APP}</Application> '<Application>{APP}</Application>' . self::NEWLINE .
<Company>{COMPANY}</Company> '<Company>{COMPANY}</Company>' . self::NEWLINE .
<Manager>{MANAGER}</Manager> '<Manager>{MANAGER}</Manager>' . self::NEWLINE .
</Properties>', '</Properties>',
'docProps/core.xml' => '<?xml version="1.0" encoding="UTF-8" standalone="yes"?> '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"> '<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> '<dcterms:created xsi:type="dcterms:W3CDTF">{DATE}</dcterms:created>' . self::NEWLINE .
<dc:title>{TITLE}</dc:title> '<dc:title>{TITLE}</dc:title>' . self::NEWLINE .
<dc:subject>{SUBJECT}</dc:subject> '<dc:subject>{SUBJECT}</dc:subject>' . self::NEWLINE .
<dc:creator>{AUTHOR}</dc:creator> '<dc:creator>{AUTHOR}</dc:creator>' . self::NEWLINE .
<cp:lastModifiedBy>{LAST_MODIFY_BY}</cp:lastModifiedBy> '<cp:lastModifiedBy>{LAST_MODIFY_BY}</cp:lastModifiedBy>' . self::NEWLINE .
<cp:keywords>{KEYWORD}</cp:keywords> '<cp:keywords>{KEYWORD}</cp:keywords>' . self::NEWLINE .
<dc:description>{DESCRIPTION}</dc:description> '<dc:description>{DESCRIPTION}</dc:description>' . self::NEWLINE .
<cp:category>{CATEGORY}</cp:category> '<cp:category>{CATEGORY}</cp:category>' . self::NEWLINE .
<dc:language>en-US</dc:language> '<dc:language>en-US</dc:language>' . self::NEWLINE .
<dcterms:modified xsi:type="dcterms:W3CDTF">{DATE}</dcterms:modified> '<dcterms:modified xsi:type="dcterms:W3CDTF">{DATE}</dcterms:modified>' . self::NEWLINE .
<cp:revision>1</cp:revision> '<cp:revision>1</cp:revision>' . self::NEWLINE .
</cp:coreProperties>', '</cp:coreProperties>',
'xl/_rels/workbook.xml.rels' => '<?xml version="1.0" encoding="UTF-8" standalone="yes"?> '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"> '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' . self::NEWLINE .
{RELS} '{RELS}' . self::NEWLINE .
</Relationships>', '</Relationships>',
'xl/worksheets/sheet1.xml' => '<?xml version="1.0" encoding="UTF-8" standalone="yes"?> '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"> '<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">' . self::NEWLINE .
<dimension ref="{REF}"/> '<dimension ref="{REF}"/>' . self::NEWLINE .
{SHEETVIEWS} '{SHEETVIEWS}' . self::NEWLINE .
{COLS} '{COLS}' . self::NEWLINE .
<sheetData>{ROWS}</sheetData> '<sheetData>' . self::NEWLINE . '{ROWS}' . self::NEWLINE . '</sheetData>' . self::NEWLINE .
{AUTOFILTER}{MERGECELLS}{HYPERLINKS} '{AUTOFILTER}' . '{MERGECELLS}' . self::NEWLINE . '{HYPERLINKS}' . self::NEWLINE .
</worksheet>', '</worksheet>',
'xl/worksheets/_rels/sheet1.xml.rels' => '<?xml version="1.0" encoding="UTF-8" standalone="yes"?> '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">{HYPERLINKS}</Relationships>', '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' . self::NEWLINE .
'xl/sharedStrings.xml' => '<?xml version="1.0" encoding="UTF-8" standalone="yes"?> '{HYPERLINKS}' . self::NEWLINE .
<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="{CNT}" uniqueCount="{CNT}">{STRINGS}</sst>', '</Relationships>',
'xl/styles.xml' => '<?xml version="1.0" encoding="UTF-8" standalone="yes"?> 'xl/sharedStrings.xml' => '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' . self::NEWLINE .
<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"> '<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="{CNT}" uniqueCount="{CNT}">' . self::NEWLINE .
{NUMFMTS} '{STRINGS}' . self::NEWLINE .
{FONTS} '</sst>',
{FILLS} 'xl/styles.xml' => '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' . self::NEWLINE .
{BORDERS} '<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">' . self::NEWLINE .
<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0" /></cellStyleXfs> '{NUMFMTS}' . self::NEWLINE .
{XF} '{FONTS}' . self::NEWLINE .
<cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles> '{FILLS}' . self::NEWLINE .
</styleSheet>', '{BORDERS}' . self::NEWLINE .
'xl/workbook.xml' => '<?xml version="1.0" encoding="UTF-8" standalone="yes"?> '<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0" /></cellStyleXfs>' . self::NEWLINE .
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"> '{XF}' . self::NEWLINE .
<fileVersion appName="{APP}"/> '<cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles>' . self::NEWLINE .
<sheets> '</styleSheet>',
{SHEETS} 'xl/workbook.xml' => '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' . self::NEWLINE .
</sheets> '<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">' . self::NEWLINE .
</workbook>', '<fileVersion appName="{APP}"/>' . self::NEWLINE .
'[Content_Types].xml' => '<?xml version="1.0" encoding="UTF-8" standalone="yes"?> '<sheets>' . self::NEWLINE .
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"> '{SHEETS}' . self::NEWLINE .
<Override PartName="/rels/.rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/> '</sheets>' . self::NEWLINE .
<Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/> '</workbook>',
<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/> '[Content_Types].xml' => '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' . self::NEWLINE .
<Override PartName="/xl/_rels/workbook.xml.rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/> '<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">' . self::NEWLINE .
<Override PartName="/xl/sharedStrings.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"/> '<Override PartName="/rels/.rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>' . self::NEWLINE .
<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/> '<Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/>' . self::NEWLINE .
<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/> '<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>' . self::NEWLINE .
{TYPES} '<Override PartName="/xl/_rels/workbook.xml.rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>' . self::NEWLINE .
</Types>', '<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"/> // <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> // <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(); ob_end_clean();
} }
fseek($fh, 0); fseek($fh, 0);
fpassthru($fh); if (function_exists('fpassthru')) fpassthru($fh);
else echo stream_get_contents($fh);
fclose($fh); fclose($fh);
return true; return true;
} }
@ -331,11 +340,10 @@ class SimpleXLSXGen
$s = ''; $s = '';
for ($i = 0; $i < $cnt_sheets; $i++) { for ($i = 0; $i < $cnt_sheets; $i++) {
$s .= '<Relationship Id="rId' . ($i + 1) . '" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"' . $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"/>'; $s .= '<Relationship Id="rId' . ($i + 2) . '" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings" Target="sharedStrings.xml"/>';
$template = str_replace('{RELS}', $s, $template); $template = str_replace('{RELS}', $s, $template);
$this->_writeEntry($fh, $cdrec, $cfilename, $template); $this->_writeEntry($fh, $cdrec, $cfilename, $template);
$entries++; $entries++;
@ -344,30 +352,38 @@ class SimpleXLSXGen
foreach ($this->sheets as $k => $v) { foreach ($this->sheets as $k => $v) {
$s .= '<sheet name="' . $this->esc($v['name']) . '" sheetId="' . ($k + 1) . '" r:id="rId' . ($k + 1) . '"/>'; $s .= '<sheet name="' . $this->esc($v['name']) . '" sheetId="' . ($k + 1) . '" r:id="rId' . ($k + 1) . '"/>';
} }
$search = ['{SHEETS}', '{APP}']; $template = str_replace(
$replace = [$s, $this->esc($this->application)]; ['{SHEETS}', '{APP}'],
$template = str_replace($search, $replace, $template); [$s, $this->esc($this->application)],
$template
);
$this->_writeEntry($fh, $cdrec, $cfilename, $template); $this->_writeEntry($fh, $cdrec, $cfilename, $template);
$entries++; $entries++;
} elseif ($cfilename === 'docProps/app.xml') { } elseif ($cfilename === 'docProps/app.xml') {
$search = ['{APP}', '{COMPANY}', '{MANAGER}']; $template = str_replace(
$replace = [$this->esc($this->application), $this->esc($this->company), $this->esc($this->manager)]; ['{APP}', '{COMPANY}', '{MANAGER}'],
$template = str_replace($search, $replace, $template); [$this->esc($this->application), $this->esc($this->company), $this->esc($this->manager)],
$template
);
$this->_writeEntry($fh, $cdrec, $cfilename, $template); $this->_writeEntry($fh, $cdrec, $cfilename, $template);
$entries++; $entries++;
} elseif ($cfilename === 'docProps/core.xml') { } elseif ($cfilename === 'docProps/core.xml') {
$search = ['{DATE}', '{AUTHOR}', '{TITLE}', '{SUBJECT}', '{KEYWORD}', '{DESCRIPTION}', '{CATEGORY}', '{LAST_MODIFY_BY}']; $template = str_replace(
$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)]; ['{DATE}', '{AUTHOR}', '{TITLE}', '{SUBJECT}', '{KEYWORD}', '{DESCRIPTION}', '{CATEGORY}', '{LAST_MODIFY_BY}'],
$template = str_replace($search, $replace, $template); [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); $this->_writeEntry($fh, $cdrec, $cfilename, $template);
$entries++; $entries++;
} elseif ($cfilename === 'xl/sharedStrings.xml') { } elseif ($cfilename === 'xl/sharedStrings.xml') {
if (!count($this->SI)) { if (!count($this->SI)) {
$this->SI[] = 'No Data'; $this->SI[] = 'No Data';
} }
$si_cnt = count($this->SI); $template = str_replace(
$si = '<si><t>' . implode("</t></si>\r\n<si><t>", $this->SI) . '</t></si>'; ['{CNT}', '{STRINGS}'],
$template = str_replace(['{CNT}', '{STRINGS}'], [$si_cnt, $si], $template); [count($this->SI), '<si><t>' . implode('</t></si>' . self::NEWLINE . '<si><t>', $this->SI) . '</t></si>'],
$template
);
$this->_writeEntry($fh, $cdrec, $cfilename, $template); $this->_writeEntry($fh, $cdrec, $cfilename, $template);
$entries++; $entries++;
} elseif ($cfilename === 'xl/worksheets/sheet1.xml') { } elseif ($cfilename === 'xl/worksheets/sheet1.xml') {
@ -385,10 +401,10 @@ class SimpleXLSXGen
$filename = 'xl/worksheets/_rels/sheet' . ($k + 1) . '.xml.rels'; $filename = 'xl/worksheets/_rels/sheet' . ($k + 1) . '.xml.rels';
foreach ($v['hyperlinks'] as $h) { foreach ($v['hyperlinks'] as $h) {
if ($h['ID']) { 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); $this->_writeEntry($fh, $cdrec, $filename, $xml);
$entries++; $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"/>'; $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); $this->_writeEntry($fh, $cdrec, $cfilename, $template);
$entries++; $entries++;
} elseif ($cfilename === 'xl/styles.xml') { } elseif ($cfilename === 'xl/styles.xml') {
@ -465,7 +481,6 @@ class SimpleXLSXGen
if ($xf[1] & self::A_WRAPTEXT) { if ($xf[1] & self::A_WRAPTEXT) {
$align .= ' wrapText="1"'; $align .= ' wrapText="1"';
} }
// border // border
$BR_ID = 0; $BR_ID = 0;
if ($xf[6] !== '') { if ($xf[6] !== '') {
@ -534,7 +549,7 @@ class SimpleXLSXGen
$template = str_replace( $template = str_replace(
['{NUMFMTS}', '{FONTS}', '{XF}', '{FILLS}', '{BORDERS}'], ['{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 $template
); );
$this->_writeEntry($fh, $cdrec, $cfilename, $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', $before_cd)); // offset to start of central dir
fwrite($fh, pack('v', mb_strlen($zipComments, '8bit'))); // .zip file comment length fwrite($fh, pack('v', mb_strlen($zipComments, '8bit'))); // .zip file comment length
fwrite($fh, $zipComments); fwrite($fh, $zipComments);
return true; return true;
} }
@ -642,24 +656,19 @@ class SimpleXLSXGen
setlocale(LC_NUMERIC, 'C'); setlocale(LC_NUMERIC, 'C');
$COLS = []; $COLS = [];
$ROWS = []; $ROWS = [];
// $SHEETVIEWS = '<sheetViews><sheetView tabSelected="1" workbookViewId="0"'.($this->rtl ? ' rightToLeft="1"' : '').'>';
$SHEETVIEWS = ''; $SHEETVIEWS = '';
$PANE = ''; $PANE = '';
if (count($this->sheets[$idx]['rows'])) { if (count($this->sheets[$idx]['rows'])) {
if ($this->sheets[$idx]['frozen'] !== '' || isset($this->sheets[$idx]['frozen'][0]) || isset($this->sheets[$idx]['frozen'][1])) { if ($this->sheets[$idx]['frozen'] !== '' || isset($this->sheets[$idx]['frozen'][0]) || isset($this->sheets[$idx]['frozen'][1])) {
// $AC = 'A1'; // Active Cell if (is_string($this->sheets[$idx]['frozen'])) { //A1 format -> store ($AC) and convert to R1C1 0-based ($y/$x)
$x = $y = 0;
if (is_string($this->sheets[$idx]['frozen'])) {
$AC = $this->sheets[$idx]['frozen']; $AC = $this->sheets[$idx]['frozen'];
self::cell2coord($AC, $x, $y); [$y, $x] = self::cell2coord($AC);
} else { $y--;
if (isset($this->sheets[$idx]['frozen'][0])) { $x--;
$x = $this->sheets[$idx]['frozen'][0]; } 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;
if (isset($this->sheets[$idx]['frozen'][1])) { $x = $this->sheets[$idx]['frozen'][1] - 1;
$y = $this->sheets[$idx]['frozen'][1]; $AC = self::coord2cell($this->sheets[$idx]['frozen']);
}
$AC = self::coord2cell($x, $y);
} }
if ($x > 0 || $y > 0) { if ($x > 0 || $y > 0) {
$split = ''; $split = '';
@ -681,10 +690,9 @@ class SimpleXLSXGen
} }
} }
if ($this->rtl || $PANE) { if ($this->rtl || $PANE) {
$SHEETVIEWS .= '<sheetViews> $SHEETVIEWS .= '<sheetViews>' . self::NEWLINE . '<sheetView workbookViewId="0"' . ($this->rtl ? ' rightToLeft="1"' : '');
<sheetView workbookViewId="0"' . ($this->rtl ? ' rightToLeft="1"' : ''); $SHEETVIEWS .= $PANE ? ('>' . self::NEWLINE . $PANE . self::NEWLINE . '</sheetView>') : ' />';
$SHEETVIEWS .= $PANE ? ">\r\n" . $PANE . "\r\n</sheetView>" : ' />'; $SHEETVIEWS .= self::NEWLINE . '</sheetViews>';
$SHEETVIEWS .= "\r\n</sheetViews>";
} }
$COLS[] = '<cols>'; $COLS[] = '<cols>';
$CUR_ROW = 0; $CUR_ROW = 0;
@ -701,7 +709,7 @@ class SimpleXLSXGen
} }
$cname = $this->num2name($CUR_COL) . $CUR_ROW; $cname = $this->num2name($CUR_COL) . $CUR_ROW;
if ($v === null || $v === '') { if ($v === null || $v === '') {
$row .= '<c r="' . $cname . '"/>'; $row .= ' <c r="' . $cname . '"/>';
continue; continue;
} }
$ct = $cv = $cf = null; $ct = $cv = $cf = null;
@ -918,15 +926,15 @@ class SimpleXLSXGen
$this->XF[] = [$N, $A, $F, $FL, $C, $BG, $BR, $FS]; $this->XF[] = [$N, $A, $F, $FL, $C, $BG, $BR, $FS];
} }
} }
$row .= '<c r="' . $cname . '"' . ($ct ? ' t="' . $ct . '"' : '') . ($cs ? ' s="' . $cs . '"' : '') . '>' $row .= ' <c r="' . $cname . '"' . ($ct ? ' t="' . $ct . '"' : '') . ($cs ? ' s="' . $cs . '"' : '') . '>'
. ($cf ? '<f>' . $cf . '</f>' : '') . ($cf ? '<f>' . $cf . '</f>' : '')
. ($ct === 'inlineStr' ? '<is><t>' . $cv . '</t></is>' : '<v>' . $cv . '</v>') . "</c>\r\n"; . ($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) { foreach ($COL as $k => $max) {
$w = isset($this->sheets[$idx]['colwidth'][$k]) ? $this->sheets[$idx]['colwidth'][$k] : min($max + 1, 60); $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>'; $COLS[] = '</cols>';
$REF = 'A1:' . $this->num2name(count($COL)) . $CUR_ROW; $REF = 'A1:' . $this->num2name(count($COL)) . $CUR_ROW;
@ -937,15 +945,15 @@ class SimpleXLSXGen
$AUTOFILTER = ''; $AUTOFILTER = '';
if ($this->sheets[$idx]['autofilter']) { if ($this->sheets[$idx]['autofilter']) {
$AUTOFILTER = '<autoFilter ref="' . $this->sheets[$idx]['autofilter'] . '" />'; $AUTOFILTER = '<autoFilter ref="' . $this->sheets[$idx]['autofilter'] . '"/>' . self::NEWLINE;
} }
$MERGECELLS = []; $MERGECELLS = [];
if (count($this->sheets[$idx]['mergecells'])) { if (count($this->sheets[$idx]['mergecells'])) {
$MERGECELLS[] = ''; //$MERGECELLS[] = '';
$MERGECELLS[] = '<mergeCells count="' . count($this->sheets[$idx]['mergecells']) . '">'; $MERGECELLS[] = '<mergeCells count="' . count($this->sheets[$idx]['mergecells']) . '">';
foreach ($this->sheets[$idx]['mergecells'] as $m) { foreach ($this->sheets[$idx]['mergecells'] as $m) {
$MERGECELLS[] = '<mergeCell ref="' . $m . '"/>'; $MERGECELLS[] = ' <mergeCell ref="' . $m . '"/>';
} }
$MERGECELLS[] = '</mergeCells>'; $MERGECELLS[] = '</mergeCells>';
} }
@ -954,7 +962,7 @@ class SimpleXLSXGen
if (count($this->sheets[$idx]['hyperlinks'])) { if (count($this->sheets[$idx]['hyperlinks'])) {
$HYPERLINKS[] = '<hyperlinks>'; $HYPERLINKS[] = '<hyperlinks>';
foreach ($this->sheets[$idx]['hyperlinks'] as $h) { 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>'; $HYPERLINKS[] = '</hyperlinks>';
} }
@ -966,17 +974,23 @@ class SimpleXLSXGen
['{REF}', '{COLS}', '{ROWS}', '{AUTOFILTER}', '{MERGECELLS}', '{HYPERLINKS}', '{SHEETVIEWS}'], ['{REF}', '{COLS}', '{ROWS}', '{AUTOFILTER}', '{MERGECELLS}', '{HYPERLINKS}', '{SHEETVIEWS}'],
[ [
$REF, $REF,
implode("\r\n", $COLS), implode(self::NEWLINE, $COLS),
implode("\r\n", $ROWS), implode(self::NEWLINE, $ROWS),
$AUTOFILTER, $AUTOFILTER,
implode("\r\n", $MERGECELLS), implode(self::NEWLINE, $MERGECELLS),
implode("\r\n", $HYPERLINKS), implode(self::NEWLINE, $HYPERLINKS),
$SHEETVIEWS $SHEETVIEWS
], ],
$template $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) public function num2name($num)
{ {
$numeric = ($num - 1) % 26; $numeric = ($num - 1) % 26;
@ -995,9 +1009,9 @@ class SimpleXLSXGen
return $excelTime; return $excelTime;
} }
// self::CALENDAR_WINDOWS_1900 // self::CALENDAR_WINDOWS_1900
$excel1900isLeapYear = True; $excel1900isLeapYear = true;
if (($year === 1900) && ($month <= 2)) { if (($year === 1900) && ($month <= 2)) {
$excel1900isLeapYear = False; $excel1900isLeapYear = false;
} }
$myExcelBaseDate = 2415020; $myExcelBaseDate = 2415020;
// Julian base date Adjustment // Julian base date Adjustment
@ -1019,13 +1033,11 @@ class SimpleXLSXGen
$this->defaultFont = $name; $this->defaultFont = $name;
return $this; return $this;
} }
public function setDefaultFontSize($size) public function setDefaultFontSize($size)
{ {
$this->defaultFontSize = $size; $this->defaultFontSize = $size;
return $this; return $this;
} }
public function setTitle($title) public function setTitle($title)
{ {
$this->title = $title; $this->title = $title;
@ -1066,7 +1078,6 @@ class SimpleXLSXGen
$this->category = $category; $this->category = $category;
return $this; return $this;
} }
public function setApplication($application) public function setApplication($application)
{ {
$this->application = $application; $this->application = $application;
@ -1083,18 +1094,17 @@ class SimpleXLSXGen
$this->sheets[$this->curSheet]['autofilter'] = $range; $this->sheets[$this->curSheet]['autofilter'] = $range;
return $this; return $this;
} }
public function mergeCells($range) public function mergeCells($range)
{ {
$this->sheets[$this->curSheet]['mergecells'][] = $range; $this->sheets[$this->curSheet]['mergecells'][] = $range;
return $this; return $this;
} }
public function setColWidth($col, $width) public function setColWidth($col, $width)
{ {
$this->sheets[$this->curSheet]['colwidth'][$col] = $width; $this->sheets[$this->curSheet]['colwidth'][$col] = $width;
return $this; return $this;
} }
public function rightToLeft($value = true) public function rightToLeft($value = true)
{ {
$this->rtl = $value; $this->rtl = $value;
@ -1122,19 +1132,31 @@ class SimpleXLSXGen
return $id; return $id;
} }
public static function raw($value) public static function raw($value)
{ {
return "\0" . $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; $x = $y = 0;
$lettercount = 0; $lettercount = 0;
$cell = str_replace([' ', '\t', '\r', '\n', '\v', '\0'], '', $cell); $cell = str_replace([' ', '\t', '\r', '\n', '\v', '\0', '$'], '', $cell);
if (empty($cell)) { if (empty($cell)) {
return; return [];
} }
$cell = strtoupper($cell); $cell = strtoupper($cell);
for ($i = 0, $len = strlen($cell); $i < $len; $i++) { for ($i = 0, $len = strlen($cell); $i < $len; $i++) {
@ -1149,24 +1171,196 @@ class SimpleXLSXGen
$x += (ord($cell[$i]) - ord('A') + 1) * (26 ** $e); $x += (ord($cell[$i]) - ord('A') + 1) * (26 ** $e);
$e++; $e++;
} }
$x++; //to make 1-based
} }
if ($lettercount < strlen($cell)) { 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 = ''; $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; $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; $temp = explode(':', $range);
return $this; 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;
}
} }