From 1d5713d6811aa71796c7aa76c3b4801e2a030830 Mon Sep 17 00:00:00 2001 From: Javier Date: Thu, 27 Jul 2023 14:12:35 -0300 Subject: [PATCH] Comments support --- README.md | 11 +++ src/SimpleXLSXGen.php | 210 ++++++++++++++++++++++++++++++++++-------- 2 files changed, 184 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 37766c2..a30258c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # SimpleXLSXGen + [](https://github.com/shuchkin/simplexlsxgen/blob/master/license.md) [](https://github.com/shuchkin/simplexlsxgen/stargazers) [](https://github.com/shuchkin/simplexlsxgen/network) [](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,6 +36,7 @@ $ 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 @@ -136,6 +140,9 @@ 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 (in A1 format as string or // R1C1 format as 2-element integer array) @@ -158,6 +165,7 @@ $xlsx->setAuthor('Sergey Shuchkin ') ->setCategory('This is Сategory') ->setApplication('Shuchkin\SimpleXLSXGen') ``` + ### JS array to Excel (AJAX) ```php 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 @@ -179,18 +179,27 @@ class SimpleXLSXGen '' . self::NEWLINE . '{RELS}' . self::NEWLINE . '', + 'xl/drawings/vmlDrawing1.vml' => '' . self::NEWLINE . + '' . self::NEWLINE . + '' . self::NEWLINE . + '{SHAPES}', 'xl/worksheets/sheet1.xml' => '' . self::NEWLINE . '' . self::NEWLINE . '' . self::NEWLINE . '{SHEETVIEWS}' . self::NEWLINE . '{COLS}' . self::NEWLINE . '' . self::NEWLINE . '{ROWS}' . self::NEWLINE . '' . self::NEWLINE . - '{AUTOFILTER}' . '{MERGECELLS}' . self::NEWLINE . '{HYPERLINKS}' . self::NEWLINE . + '{AUTOFILTER}' . '{MERGECELLS}' . self::NEWLINE . '{HYPERLINKS}' . self::NEWLINE . '{COMMENTS}' . '', 'xl/worksheets/_rels/sheet1.xml.rels' => '' . self::NEWLINE . '' . self::NEWLINE . '{HYPERLINKS}' . self::NEWLINE . '', + 'xl/comments1.xml' => '' . self::NEWLINE . + '' . self::NEWLINE . + '{AUTHORS}' . self::NEWLINE . + '{COMMENTS}' . self::NEWLINE . + '', 'xl/sharedStrings.xml' => '' . self::NEWLINE . '' . self::NEWLINE . '{STRINGS}' . self::NEWLINE . @@ -258,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; } @@ -340,22 +361,22 @@ class SimpleXLSXGen if ($cfilename === 'xl/_rels/workbook.xml.rels') { $s = ''; for ($i = 0; $i < $cnt_sheets; $i++) { - $s .= '' . self::NEWLINE; + $s .= '' . self::NEWLINE; } - $s .= '' . self::NEWLINE; - $s .= ''; + $s .= '' . self::NEWLINE; + $s .= ''; $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 .= ''; + $s .= ''; } $template = str_replace( ['{SHEETS}', '{APP}'], - [$s, $this->esc($this->application)], + [$s, self::esc($this->application)], $template ); $this->_writeEntry($fh, $cdrec, $cfilename, $template); @@ -363,7 +384,7 @@ class SimpleXLSXGen } elseif ($cfilename === 'docProps/app.xml') { $template = str_replace( ['{APP}', '{COMPANY}', '{MANAGER}'], - [$this->esc($this->application), $this->esc($this->company), $this->esc($this->manager)], + [self::esc($this->application), self::esc($this->company), self::esc($this->manager)], $template ); $this->_writeEntry($fh, $cdrec, $cfilename, $template); @@ -371,7 +392,7 @@ class SimpleXLSXGen } elseif ($cfilename === 'docProps/core.xml') { $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)], + [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); @@ -395,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 = '' . self::NEWLINE; + for ($i = 0; $i < $cAuthors; $i++) { + $AUTHORS .= ' ' . $v['authors'][$i] . '' . self::NEWLINE; + if ($v['authors'][$i] === __CLASS__) $authorId = $i; + } + if ($authorId === -1) { + $authorId = $cAuthors; + $AUTHORS .= ' ' . __CLASS__ . '' . self::NEWLINE; + } + $AUTHORS .= ''; + //comments list + $COMMENTS = '' . self::NEWLINE; + for ($i = 0; $i < $cComments; $i++) { + $COMMENTS .= '' . self::NEWLINE; + //prefix with author name if exist + if ($v['comments'][$i]['authorId'] !== -1) { + $COMMENTS .= ' ' . $v['authors'][$v['comments'][$i]['authorId']] . ':' . self::NEWLINE . ''; + } + $preserve = (strpbrk($v['comments'][$i]['comment'], "\t\r\n") !== false) ? ' xml:space="preserve"' : ''; + $COMMENTS .= ' ' . $v['comments'][$i]['comment'] . '' . self::NEWLINE; + $COMMENTS .= '' . self::NEWLINE; + } + $COMMENTS .= ''; + $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 .= '' . 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) { @@ -402,9 +487,13 @@ class SimpleXLSXGen $filename = 'xl/worksheets/_rels/sheet' . ($k + 1) . '.xml.rels'; foreach ($v['hyperlinks'] as $h) { if ($h['ID']) { - $RH[] = ' '; + $RH[] = ' '; } } + if ($v['commentDrawingId']) { + $RH[] = ' '; + $RH[] = ' '; + } $xml = str_replace('{HYPERLINKS}', implode(self::NEWLINE, $RH), $template); $this->_writeEntry($fh, $cdrec, $filename, $xml); $entries++; @@ -412,13 +501,20 @@ class SimpleXLSXGen } $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 = ['']; foreach ($this->sheets as $k => $v) { $TYPES[] = ''; if ($this->extLinkId) { $TYPES[] = ''; } + if (count($v['comments'])) { + $hasComments = true; + $TYPES[] = ''; + } } + if ($hasComments) $TYPES[] = ''; $template = str_replace('{TYPES}', implode(self::NEWLINE, $TYPES), $template); $this->_writeEntry($fh, $cdrec, $cfilename, $template); $entries++; @@ -499,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) { @@ -710,7 +806,7 @@ class SimpleXLSXGen } $cname = $this->num2name($CUR_COL) . $CUR_ROW; if ($v === null || $v === '') { - $row .= ' '; + $row .= ' '; continue; } $ct = $cv = $cf = null; @@ -873,7 +969,7 @@ class SimpleXLSXGen } } if ($cv === null) { - $v = $this->esc($v); + $v = self::esc($v); if ($cf) { $ct = 'str'; $cv = $v; @@ -963,16 +1059,23 @@ class SimpleXLSXGen if (count($this->sheets[$idx]['hyperlinks'])) { $HYPERLINKS[] = ''; foreach ($this->sheets[$idx]['hyperlinks'] as $h) { - $HYPERLINKS[] = ' '; + $HYPERLINKS[] = ' '; } $HYPERLINKS[] = ''; } + $COMMENTS = ''; + if (count($this->sheets[$idx]['comments'])) { + $this->extLinkId++; + $this->sheets[$idx]['commentDrawingId'] = $this->extLinkId; + $COMMENTS = '' . 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(self::NEWLINE, $COLS), @@ -980,7 +1083,8 @@ class SimpleXLSXGen $AUTOFILTER, implode(self::NEWLINE, $MERGECELLS), implode(self::NEWLINE, $HYPERLINKS), - $SHEETVIEWS + $SHEETVIEWS, + $COMMENTS ], $template ); @@ -1089,6 +1193,11 @@ class SimpleXLSXGen $this->lastModifiedBy = $lastModifiedBy; return $this; } + public function rightToLeft($value = true) + { + $this->rtl = $value; + return $this; + } public function autoFilter($range) { @@ -1105,18 +1214,44 @@ class SimpleXLSXGen $this->sheets[$this->curSheet]['colwidth'][$col] = $width; return $this; } - - public function rightToLeft($value = true) + public function freezePanes($cell) { - $this->rtl = $value; + $this->sheets[$this->curSheet]['frozen'] = $cell; return $this; } - - public function esc($str) + /** + * 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 = '') { - // XML UTF-8: #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF] - // but we use fast version - return str_replace(['&', '<', '>', "\x00", "\x03", "\x0B"], ['&', '<', '>', '', '', ''], $str); + 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) @@ -1133,17 +1268,18 @@ 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"], ['&', '<', '>', '', '', ''], $str); + } + public static function raw($value) { return "\0" . $value; } - public function freezePanes($cell) - { - $this->sheets[$this->curSheet]['frozen'] = $cell; - return $this; - } - /** * Convert A1 cell reference format to R1C1 cell reference format (row/col number starting from 1) *