This commit is contained in:
Javier 2023-08-06 10:47:43 -03:00 committed by GitHub
commit f47f9cbda8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 617 additions and 184 deletions

View File

@ -1,4 +1,5 @@
# SimpleXLSXGen
[<img src="https://img.shields.io/github/license/shuchkin/simplexlsxgen" />](https://github.com/shuchkin/simplexlsxgen/blob/master/license.md) [<img src="https://img.shields.io/github/stars/shuchkin/simplexlsxgen" />](https://github.com/shuchkin/simplexlsxgen/stargazers) [<img src="https://img.shields.io/github/forks/shuchkin/simplexlsxgen" />](https://github.com/shuchkin/simplexlsxgen/network) [<img src="https://img.shields.io/github/issues/shuchkin/simplexlsxgen" />](https://github.com/shuchkin/simplexlsxgen/issues)
Export data to Excel XLSX file. PHP XLSX generator. No external tools and libraries.
@ -11,6 +12,7 @@ Export data to Excel XLSX file. PHP XLSX generator. No external tools and librar
*Hey, bro, please ★ the package for my motivation :) and [donate](https://opencollective.com/simplexlsx) for more motivation!*
## Basic Usage
```php
$books = [
['ISBN', 'title', 'author', 'publisher', 'ctry' ],
@ -23,6 +25,7 @@ $xlsx->saveAs('books.xlsx'); // or downloadAs('books.xlsx') or $xlsx_content = (
![XLSX screenshot](books.png)
## Installation
The recommended way to install this library is [through Composer](https://getcomposer.org).
[New to Composer?](https://getcomposer.org/doc/00-intro.md)
@ -33,7 +36,9 @@ $ composer require shuchkin/simplexlsxgen
or download class [here](https://github.com/shuchkin/simplexlsxgen/blob/master/src/SimpleXLSXGen.php)
## Examples
Use UTF-8 encoded strings.
### Data types
```php
$data = [
@ -127,16 +132,20 @@ $xlsx_cache = (string) (new Shuchkin\SimpleXLSXGen)->addSheet( $books, 'Modern s
// Classic interface
use Shuchkin\SimpleXLSXGen
$xlsx = new SimpleXLSXGen();
$xlsx->addSheet( $books, 'Catalog 2021' );
$xlsx->addSheet( $books2, 'Stephen King catalog');
$xlsx->addSheet($books, 'Catalog 2021');
$xlsx->addSheet($books2, 'Stephen King catalog');
$xlsx->downloadAs('books_2021.xlsx');
exit();
// Autofilter
$xlsx->autoFilter('A1:B10');
// Comment cell
$xlsx->setComment("B2", "Comment text", "Comment author");
// 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
@ -156,6 +165,7 @@ $xlsx->setAuthor('Sergey Shuchkin <sergey.shuchkin@gmail.com>')
->setCategory('This is Сategory')
->setApplication('Shuchkin\SimpleXLSXGen')
```
### JS array to Excel (AJAX)
```php
<?php // array2excel.php
@ -217,7 +227,16 @@ function array2excel() {
</html>
```
## Notes
* When XLSX file is generated (__toString, saveAs or downloadAs methods), the data matrix of each sheet is traversed by rows and columns using foreach cycles. This implies freedom in the type of indexes used for the data but it also implies paying special attention when using numeric indexes since the array is traversed in the order in which they were defined and not in the numerical order of said indexes.
* The helper methods for setting and getting the styles of a range or cell, implies that the data was defined as a 2-dimensional array (matrix) with consecutive 0-based numeric indexes.
* If you use download or downloadAs methods, make sure you don't generate output before (not important if you use output buffering) and after you call the method. Pay special attention to whitespaces characters (space, cr/lf, tab) in source files before and after PHP code closing tags. If you inadvertently, or on purpose, generate text output, the resulting XLSX file will be corrupted.
* Methods setDefaultFont, setDefaultFontSize, setTitle, setSubject, setAuthor, setCompany, setManager, setKeywords, setDescription, setCategory, setApplication, setLastModifiedBy and rightToLeft apply to the entire book.
* Methods autoFilter, mergeCells, setColWidth, freezePanes and setComment apply to the current sheet (the last addSheet/fromArray used)
## Debug
```php
ini_set('error_reporting', E_ALL );
ini_set('display_errors', 1 );

View File

@ -3,15 +3,16 @@
/** @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 +41,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 +91,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 = '';
@ -106,9 +110,9 @@ class SimpleXLSXGen
$this->defaultFont = 'Calibri';
$this->defaultFontSize = 10;
$this->rtl = false;
$this->sheets = [['name' => 'Sheet1', 'rows' => [], 'hyperlinks' => [], 'mergecells' => [], 'colwidth' => [], 'autofilter' => '']];
$this->sheets = [];
$this->extLinkId = 0;
$this->SI = []; // sharedStrings index
$this->SI = []; // sharedStrings index
$this->SI_KEYS = []; // & keys
// https://c-rex.net/projects/samples/ooxml/e1/Part4/OOXML_P4_DOCX_numFmts_topic_ID0E6KK6.html
@ -142,78 +146,92 @@ 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/drawings/vmlDrawing1.vml' => '<xml xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel">' . self::NEWLINE .
'<o:shapelayout v:ext="edit"><o:idmap v:ext="edit" data="1"/></o:shapelayout>' . self::NEWLINE .
'<v:shapetype id="_x0000_t202" coordsize="21600,21600" o:spt="202" path="m,l,21600r21600,l21600,xe"><v:stroke joinstyle="miter"/><v:path gradientshapeok="t" o:connecttype="rect"/></v:shapetype>' . self::NEWLINE .
'{SHAPES}</xml>',
'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 . '{COMMENTS}' .
'</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/comments1.xml' => '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' . self::NEWLINE .
'<comments xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">' . self::NEWLINE .
'{AUTHORS}' . self::NEWLINE .
'{COMMENTS}' . self::NEWLINE .
'</comments>',
'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>
@ -249,11 +267,23 @@ class SimpleXLSXGen
}
}
}
$this->sheets[$this->curSheet] = ['name' => $name, 'hyperlinks' => [], 'mergecells' => [], 'colwidth' => [], 'autofilter' => '', 'frozen' => ''];
if (isset($rows[0]) && is_array($rows[0])) {
$this->sheets[$this->curSheet]['rows'] = $rows;
} else {
$this->sheets[$this->curSheet]['rows'] = [];
$this->sheets[$this->curSheet] = [
'name' => $name,
'rows' => [],
'hyperlinks' => [],
'mergecells' => [],
'colwidth' => [],
'autofilter' => '',
'frozen' => '',
'authors' => [],
'comments' => [],
'commentDrawingId' => null
];
if (is_array($rows)) {
foreach ($rows as $row) {
if (is_array($row)) $this->sheets[$this->curSheet]['rows'] = $rows;
break;
}
}
return $this;
}
@ -311,7 +341,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;
}
@ -330,44 +361,51 @@ class SimpleXLSXGen
if ($cfilename === 'xl/_rels/workbook.xml.rels') {
$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";
$s .= '<Relationship Id="rId' . ($i + 1) . '" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" ' .
'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 + 2) . '" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings" Target="sharedStrings.xml"/>';
$s .= '<Relationship Id="rId' . ($cnt_sheets + 1) . '" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>' . self::NEWLINE;
$s .= '<Relationship Id="rId' . ($cnt_sheets + 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++;
} elseif ($cfilename === 'xl/workbook.xml') {
$s = '';
foreach ($this->sheets as $k => $v) {
$s .= '<sheet name="' . $this->esc($v['name']) . '" sheetId="' . ($k + 1) . '" r:id="rId' . ($k + 1) . '"/>';
$s .= '<sheet name="' . self::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, self::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}'],
[self::esc($this->application), self::esc($this->company), self::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'), self::esc($this->author), self::esc($this->title), self::esc($this->subject), self::esc($this->keywords), self::esc($this->description), self::esc($this->category), self::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') {
@ -378,6 +416,70 @@ class SimpleXLSXGen
$entries++;
}
$xml = null;
} elseif ($cfilename === 'xl/comments1.xml') {
foreach ($this->sheets as $k => $v) {
$cComments = count($v['comments']);
$cAuthors = count($v['authors']);
if ($cComments) {
$filename = 'xl/comments' . ($k + 1) . '.xml';
//create author(s) list and set author Id
$authorId = -1; //index for document author to use in comments
$AUTHORS = '<authors>' . self::NEWLINE;
for ($i = 0; $i < $cAuthors; $i++) {
$AUTHORS .= ' <author>' . $v['authors'][$i] . '</author>' . self::NEWLINE;
if ($v['authors'][$i] === __CLASS__) $authorId = $i;
}
if ($authorId === -1) {
$authorId = $cAuthors;
$AUTHORS .= ' <author>' . __CLASS__ . '</author>' . self::NEWLINE;
}
$AUTHORS .= '</authors>';
//comments list
$COMMENTS = '<commentList>' . self::NEWLINE;
for ($i = 0; $i < $cComments; $i++) {
$COMMENTS .= '<comment ref="' . $v['comments'][$i]['cell'] . '" authorId="' . (($v['comments'][$i]['authorId'] !== -1) ? $v['comments'][$i]['authorId'] : $authorId) . '" shapeId="0"><text>' . self::NEWLINE;
//prefix with author name if exist
if ($v['comments'][$i]['authorId'] !== -1) {
$COMMENTS .= ' <r><rPr><b></b><sz val="9"/><rFont val="Tahoma"/><charset val="1"/></rPr><t xml:space="preserve">' . $v['authors'][$v['comments'][$i]['authorId']] . ':' . self::NEWLINE . '</t></r>';
}
$preserve = (strpbrk($v['comments'][$i]['comment'], "\t\r\n") !== false) ? ' xml:space="preserve"' : '';
$COMMENTS .= ' <r><rPr><sz val="9"/><rFont val="Tahoma"/><charset val="1"/></rPr><t' . $preserve . '>' . $v['comments'][$i]['comment'] . '</t></r>' . self::NEWLINE;
$COMMENTS .= '</text></comment>' . self::NEWLINE;
}
$COMMENTS .= '</commentList>';
$xml = str_replace(['{AUTHORS}', '{COMMENTS}'], [$AUTHORS, $COMMENTS], $template);
$this->_writeEntry($fh, $cdrec, $filename, $xml);
$entries++;
}
}
$xml = null;
} elseif ($cfilename === 'xl/drawings/vmlDrawing1.vml') {
foreach ($this->sheets as $k => $v) {
$cComments = count($v['comments']);
if ($cComments) {
$SHAPES = '';
$filename = 'xl/drawings/vmlDrawing' . ($k + 1) . '.vml';
for ($i = 0; $i < $cComments; $i++) {
[$row, $col] = self::cell2coord($v['comments'][$i]['cell']);
$SHAPES .= '<v:shape id="_x0000_s' . $v['comments'][$i]['cell'] . '" type="#_x0000_t202" style="z-index:2;visibility:hidden" fillcolor="#ffffe1">' . self::NEWLINE .
' <v:fill color2="#ffffe1"/>' . self::NEWLINE .
' <v:path o:connecttype="none"/>' . self::NEWLINE .
' <x:ClientData ObjectType="Note">' . self::NEWLINE .
' <x:MoveWithCells/>' . self::NEWLINE .
' <x:SizeWithCells/>' . self::NEWLINE .
' <x:Anchor>' . $col . ', 0, ' . $row . ', 0, ' . ($col+2) . ', 0, ' . ($row+3) . ', 0</x:Anchor>' . self::NEWLINE .
' <x:AutoFill>False</x:AutoFill>' . self::NEWLINE .
' <x:Row>' . ($row-1) . '</x:Row>' . self::NEWLINE .
' <x:Column>' . ($col-1) . '</x:Column>' . self::NEWLINE .
' </x:ClientData>' . self::NEWLINE .
'</v:shape>' . self::NEWLINE;
}
$xml = str_replace('{SHAPES}', $SHAPES, $template);
$this->_writeEntry($fh, $cdrec, $filename, $xml);
$entries++;
}
}
$xml = null;
} elseif ($cfilename === 'xl/worksheets/_rels/sheet1.xml.rels') {
foreach ($this->sheets as $k => $v) {
if ($this->extLinkId) {
@ -385,24 +487,35 @@ 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="' . self::esc($h['H']) . '" TargetMode="External"/>';
}
}
$xml = str_replace('{HYPERLINKS}', implode("\r\n", $RH), $template);
if ($v['commentDrawingId']) {
$RH[] = ' <Relationship Id="rId' . $v['commentDrawingId'] . '" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing" Target="../drawings/vmlDrawing' . ($k + 1) . '.vml"/>';
$RH[] = ' <Relationship Id="rId' . ($v['commentDrawingId'] + 1) . '" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments" Target="../comments' . ($k + 1) . '.xml"/>';
}
$xml = str_replace('{HYPERLINKS}', implode(self::NEWLINE, $RH), $template);
$this->_writeEntry($fh, $cdrec, $filename, $xml);
$entries++;
}
}
$xml = null;
} elseif ($cfilename === '[Content_Types].xml') {
$hasComments = false;
//if $TYPES is initialized always with this value, why not add it to the template?
$TYPES = ['<Override PartName="/_rels/.rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>'];
foreach ($this->sheets as $k => $v) {
$TYPES[] = '<Override PartName="/xl/worksheets/sheet' . ($k + 1) . '.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>';
if ($this->extLinkId) {
$TYPES[] = '<Override PartName="/xl/worksheets/_rels/sheet' . ($k + 1) . '.xml.rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>';
}
if (count($v['comments'])) {
$hasComments = true;
$TYPES[] = '<Override PartName="/xl/comments' . ($k + 1) . '.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml"/>';
}
}
$template = str_replace('{TYPES}', implode("\r\n", $TYPES), $template);
if ($hasComments) $TYPES[] = '<Default Extension="vml" ContentType="application/vnd.openxmlformats-officedocument.vmlDrawing"/>';
$template = str_replace('{TYPES}', implode(self::NEWLINE, $TYPES), $template);
$this->_writeEntry($fh, $cdrec, $cfilename, $template);
$entries++;
} elseif ($cfilename === 'xl/styles.xml') {
@ -465,7 +578,6 @@ class SimpleXLSXGen
if ($xf[1] & self::A_WRAPTEXT) {
$align .= ' wrapText="1"';
}
// border
$BR_ID = 0;
if ($xf[6] !== '') {
@ -483,7 +595,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 +646,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 +668,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 +753,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 +787,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 +806,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;
@ -864,7 +969,7 @@ class SimpleXLSXGen
}
}
if ($cv === null) {
$v = $this->esc($v);
$v = self::esc($v);
if ($cf) {
$ct = 'str';
$cv = $v;
@ -918,34 +1023,34 @@ 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;
} else {
$ROWS[] = '<row r="1"><c r="A1" t="s"><v>0</v></c></row>';
$ROWS[] = '<row r="1"><c r="A1"></c></row>'; //'<row r="1"><c r="A1" t="s"><v>0</v></c></row>';
$REF = 'A1:A1';
}
$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,29 +1059,43 @@ 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="' . self::esc($h['L']) . '" display="' . self::esc($h['H'] . ($h['L'] ? ' - ' . $h['L'] : '')) . '"/>';
}
$HYPERLINKS[] = '</hyperlinks>';
}
$COMMENTS = '';
if (count($this->sheets[$idx]['comments'])) {
$this->extLinkId += 2; //2 links: drawing in sheet?.xml and drawing and comments?.xml in rel file
$this->sheets[$idx]['commentDrawingId'] = $this->extLinkId - 1;
$COMMENTS = '<legacyDrawing r:id="rId' . $this->sheets[$idx]['commentDrawingId'] . '"/>' . self::NEWLINE;
}
//restore locale
setlocale(LC_NUMERIC, $_loc);
return str_replace(
['{REF}', '{COLS}', '{ROWS}', '{AUTOFILTER}', '{MERGECELLS}', '{HYPERLINKS}', '{SHEETVIEWS}'],
['{REF}', '{COLS}', '{ROWS}', '{AUTOFILTER}', '{MERGECELLS}', '{HYPERLINKS}', '{SHEETVIEWS}', '{COMMENTS}'],
[
$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),
$SHEETVIEWS
implode(self::NEWLINE, $MERGECELLS),
implode(self::NEWLINE, $HYPERLINKS),
$SHEETVIEWS,
$COMMENTS
],
$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 +1114,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 +1138,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 +1183,6 @@ class SimpleXLSXGen
$this->category = $category;
return $this;
}
public function setApplication($application)
{
$this->application = $application;
@ -1077,35 +1193,65 @@ class SimpleXLSXGen
$this->lastModifiedBy = $lastModifiedBy;
return $this;
}
public function autoFilter($range)
{
$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;
return $this;
}
public function esc($str)
public function autoFilter($range)
{
// XML UTF-8: #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
// but we use fast version
return str_replace(['&', '<', '>', "\x00", "\x03", "\x0B"], ['&amp;', '&lt;', '&gt;', '', '', ''], $str);
$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 freezePanes($cell)
{
$this->sheets[$this->curSheet]['frozen'] = $cell;
return $this;
}
/**
* Set comment of a cell of the current sheet
*
* @param string|array $cell Cell reference in A1 format (string) or R1C1 format (2-element array)
* @param string $comment Text comment
* @param string $author Optional. If specified, the comment will have $author as a preffix (in bold) of comment
*
* @return void
*/
public function setComment($cell, $comment, $author = '')
{
if (is_array($cell)) {
$cell = self::coord2cell($cell);
} else {
$cell = trim(strtoupper($cell));
}
//search author index
$index = -1;
if ($author !== '') {
$cAuthors = count($this->sheets[$this->curSheet]['authors']);
for ($i = 0; $i < $cAuthors; $i++) {
if ($author === $this->sheets[$this->curSheet]['authors'][$i]) {
$index = $i;
break;
}
}
if ($index === -1) {
$this->sheets[$this->curSheet]['authors'][] = $author;
$index = $cAuthors;
}
}
$this->sheets[$this->curSheet]['comments'][] = ['cell' => $cell, 'comment' => $comment, 'authorId' => $index];
return $this;
}
public function getNumFmtId($code)
@ -1122,19 +1268,32 @@ class SimpleXLSXGen
return $id;
}
public static function esc($str)
{
// XML UTF-8: #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
// but we use fast version
return str_replace(['&', '<', '>', "\x00", "\x03", "\x0B"], ['&amp;', '&lt;', '&gt;', '', '', ''], $str);
}
public static function raw($value)
{
return "\0" . $value;
}
public static function cell2coord($cell, &$x, &$y)
/**
* Convert A1 cell reference format to R1C1 cell reference format (row/col number starting from 1)
*
* @param string $cell Cell reference in A1 format
*
* @return array Cell reference in R1C1 format as a 2-element integer 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 +1308,33 @@ 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 format (row/col 1-based) to A1 cell reference format
*
* @param integer $y Row number (starting from 1) or a 2-element integer array with cell 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)
{
$this->sheets[$this->curSheet]['frozen'] = $cell;
return $this;
}
}

246
src/SimpleXLSXGenEx.php Normal file
View File

@ -0,0 +1,246 @@
<?php
/** @noinspection ReturnTypeCanBeDeclaredInspection */
/** @noinspection PhpMissingReturnTypeInspection */
/** @noinspection NullCoalescingOperatorCanBeUsedInspection */
/** @noinspection PhpIssetCanBeReplacedWithCoalesceInspection */
namespace Shuchkin;
/**
* Class SimpleXLSXGenEx
*
* Helper methods to manage sheet data. It needs SimpleXLSXGen class.
*
* @author Sergey Shuchkin <sergey.shuchkin@gmail.com>
*/
class SimpleXLSXGenEx
{
const EDGE_LEFT = 1;
const EDGE_RIGHT = 2;
const EDGE_TOP = 4;
const EDGE_BOTTOM = 8;
const EDGE_ALL = self::EDGE_LEFT + self::EDGE_RIGHT + self::EDGE_TOP + self::EDGE_BOTTOM;
/**
* Convert A1 cell reference format to R1C1 cell reference format (row/col number starting from 1)
*
* @param string $cell Cell reference in A1 format
*
* @return array Cell reference in R1C1 format as a 2-element integer 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);
if (empty($cell)) {
return [];
}
$cell = strtoupper($cell);
for ($i = 0, $len = strlen($cell); $i < $len; $i++) {
if ($cell[$i] >= 'A' && $cell[$i] <= 'Z') {
$lettercount++;
}
}
if ($lettercount > 0) {
$x = ord($cell[$lettercount - 1]) - ord('A');
$e = 1;
for ($i = $lettercount - 2; $i >= 0; $i--) {
$x += (ord($cell[$i]) - ord('A') + 1) * (26 ** $e);
$e++;
}
$x++; //to make 1-based
}
if ($lettercount < strlen($cell)) {
$y = ((int)substr($cell, $lettercount));
}
return [$y, $x];
}
/**
* Convert R1C1 cell reference format (row/col 1-based) to A1 cell reference format
*
* @param integer $y Row number (starting from 1) or a 2-element integer array with cell 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 - 1; $i >= 0; $i = ((int)($i / 26)) - 1) {
$c = chr(ord('A') + $i % 26) . $c;
}
return $c . strval($y);
}
/**
* Convert A1 range reference format to R1C1 range reference format
*
* @param string $range Range reference in A1 format
*
* @return array Range reference in R1C1 format as a 4-element integer array: [top-left row, top-left col, bottom-right row, bottom-right col]
*/
public static function range2coord($range)
{
$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 format (row/col 1-based) to A1 range reference format
*
* 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 integer elements array)
* @param array $style Associative array with style attributes name/value pairs to change/add. You can use wildcard for
* specific borders to retain original value.
* @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 or-ing) the constants EDGE_TOP, EDGE_RIGHT, EDGE_BOTTOM, EDGE_LEFT
* @return void
*/
public static function setCellStyle(&$data, $cell, $style, $edge = self::EDGE_ALL)
{
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
//border processing
if (array_key_exists('border', $style)) {
//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 as indicated by $edge, except when $border has wildcard
if ($edge & self::EDGE_TOP && $border[0] != '*') $newBorder[0] = $border[0];
if ($edge & self::EDGE_RIGHT && $border[1] != '*') $newBorder[1] = $border[1];
if ($edge & self::EDGE_BOTTOM && $border[2] != '*') $newBorder[2] = $border[2];
if ($edge & self::EDGE_LEFT && $border[3] != '*') $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 integer 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::EDGE_TOP + self::EDGE_LEFT); //top-left corner
for ($i = $range[1] + 1; $i < $range[3]; $i++) self::setCellStyle($data, [$range[0], $i], $style, self::EDGE_TOP); //loop top edge (except first and last column)
self::setCellStyle($data, [$range[0], $range[3]], $style, self::EDGE_TOP + self::EDGE_RIGHT); //top-right corner
for ($i = $range[0] + 1; $i < $range[2]; $i++) self::setCellStyle($data, [$i, $range[3]], $style, self::EDGE_RIGHT); //loop right edge (except top and bottom row)
self::setCellStyle($data, [$range[2], $range[3]], $style, self::EDGE_BOTTOM + self::EDGE_RIGHT); //bottom-right corner
for ($i = $range[3] - 1; $i > $range[1]; $i--) self::setCellStyle($data, [$range[2], $i], $style, self::EDGE_BOTTOM); //loop bottom edge (except right and left column)
self::setCellStyle($data, [$range[2], $range[1]], $style, self::EDGE_BOTTOM + self::EDGE_LEFT); //bottom-left corner
for ($i = $range[2] - 1; $i > $range[0]; $i--) self::setCellStyle($data, [$i, $range[1]], $style, self::EDGE_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 string Modified text
*/
public static function setTagAttributes($str, $tag, $attributes)
{
if (empty($attributes)) return $str;
$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;
}
}