From 3abb47b9b4730f9bcdf4ff17af70133005ea5b5b Mon Sep 17 00:00:00 2001 From: Savino Rapillo Date: Mon, 2 Nov 2020 17:34:19 +0100 Subject: [PATCH] added multisheet support (keeping it compatible to the original version) --- src/SimpleXLSXGen.php | 315 +++++++++++++++++++++++++++--------------- 1 file changed, 200 insertions(+), 115 deletions(-) diff --git a/src/SimpleXLSXGen.php b/src/SimpleXLSXGen.php index 66b95b4..de5c316 100644 --- a/src/SimpleXLSXGen.php +++ b/src/SimpleXLSXGen.php @@ -7,10 +7,17 @@ class SimpleXLSXGen { - public $rows; - public $template; + public $curSheet; + private $sheets; + private $template; + private $SI, $SI_KEYS; + public function __construct() { - $this->rows = []; + $this->curSheet = 0; + $this->sheets[0]['rows'] = []; + $this->sheets[0]['Name'] = 'Sheet1'; + $this->SI = []; // sharedStrings index & keys + $this->SI_KEYS = []; $this->template = [ '[Content_Types].xml' => ' @@ -18,12 +25,11 @@ class SimpleXLSXGen { - +{SHEETS} ', -/* */ '_rels/.rels' => ' @@ -44,8 +50,7 @@ class SimpleXLSXGen { 'xl/_rels/workbook.xml.rels' => ' - -', +{SHEETS}', 'xl/worksheets/sheet1.xml' => ' {COLS}{ROWS}', 'xl/sharedStrings.xml' => ' @@ -70,26 +75,45 @@ class SimpleXLSXGen { ', 'xl/workbook.xml' => ' - - -' + +{SHEETS} +' ]; // // 01001200 // Простой шаблонБудем делать генератор } + + public function setSheet($idx, $name = null) { + if (!isset($this->sheets[$idx])) { + $this->sheets[$idx] = []; + $this->sheets[$idx]['rows'] = []; + } + $this->curSheet = $idx; + if ($name) + $this->sheets[$idx]['Name'] = $name; + else { + if (!isset($this->sheets[$idx]['Name'])) { + $this->sheets[$idx]['Name'] = 'Sheet'.$idx; + } + } + return $this->sheets[$idx]['Name']; + } + public static function fromArray( array $rows ) { $xlsx = new self(); $xlsx->setRows( $rows ); return $xlsx; } + public function setRows( $rows ) { if ( is_array( $rows ) && isset( $rows[0] ) && is_array($rows[0]) ) { - $this->rows = $rows; + $this->sheets[$this->curSheet]['rows']=$rows; } else { - $this->rows = []; + $this->sheets[$this->curSheet]['rows']= []; } } + public function __toString() { $fh = fopen( 'php://memory', 'wb' ); if ( ! $fh ) { @@ -105,6 +129,7 @@ class SimpleXLSXGen { return (string) fread( $fh, $size ); } + public function saveAs( $filename ) { $fh = fopen( $filename, 'wb' ); if (!$fh) { @@ -118,9 +143,11 @@ class SimpleXLSXGen { return true; } + public function download() { return $this->downloadAs( gmdate('YmdHi') . '.xlsx' ); } + public function downloadAs( $filename ) { $fh = fopen('php://memory','wb'); if (!$fh) { @@ -149,28 +176,95 @@ class SimpleXLSXGen { return true; } - private function _generate( $fh ) { - + private function _saveIt($fh, &$cdrec, $cfilename, &$data) { $zipSignature = "\x50\x4b\x03\x04"; // local file header signature $dirSignature = "\x50\x4b\x01\x02"; // central dir header signature - $dirSignatureE= "\x50\x4b\x05\x06"; // end of central dir signature - $zipComments = 'Generated by '.__CLASS__.' PHP class, thanks sergey.shuchkin@gmail.com'; + $e = []; + $e['uncsize'] = mb_strlen($data, '8bit'); -// $fh = fopen( $filename, 'wb' ); - - if (!$fh) { - return false; + // if data to compress is too small, just store it + if($e['uncsize'] < 256){ + $e['comsize'] = $e['uncsize']; + $e['vneeded'] = 10; + $e['cmethod'] = 0; + $zdata = $data; + } else{ // otherwise, compress it + $zdata = gzcompress($data); + $zdata = substr(substr($zdata, 0, - 4 ), 2); // fix crc bug (thanks to Eric Mueller) + $e['comsize'] = mb_strlen($zdata, '8bit'); + $e['vneeded'] = 10; + $e['cmethod'] = 8; } - $SI = []; - $SI_KEYS = []; + $e['bitflag'] = 0; + $e['crc_32'] = crc32($data); + + // Convert date and time to DOS Format, and set then + $lastmod_timeS = str_pad(decbin(date('s')>=32?date('s')-32:date('s')), 5, '0', STR_PAD_LEFT); + $lastmod_timeM = str_pad(decbin(date('i')), 6, '0', STR_PAD_LEFT); + $lastmod_timeH = str_pad(decbin(date('H')), 5, '0', STR_PAD_LEFT); + $lastmod_dateD = str_pad(decbin(date('d')), 5, '0', STR_PAD_LEFT); + $lastmod_dateM = str_pad(decbin(date('m')), 4, '0', STR_PAD_LEFT); + $lastmod_dateY = str_pad(decbin(date('Y')-1980), 7, '0', STR_PAD_LEFT); + + # echo "ModTime: $lastmod_timeS-$lastmod_timeM-$lastmod_timeH (".date("s H H").")\n"; + # echo "ModDate: $lastmod_dateD-$lastmod_dateM-$lastmod_dateY (".date("d m Y").")\n"; + $e['modtime'] = bindec("$lastmod_timeH$lastmod_timeM$lastmod_timeS"); + $e['moddate'] = bindec("$lastmod_dateY$lastmod_dateM$lastmod_dateD"); + + $e['offset'] = ftell($fh); + + /** @noinspection DisconnectedForeachInstructionInspection */ + fwrite($fh, $zipSignature); + fwrite($fh, pack('s', $e['vneeded'])); // version_needed + fwrite($fh, pack('s', $e['bitflag'])); // general_bit_flag + fwrite($fh, pack('s', $e['cmethod'])); // compression_method + fwrite($fh, pack('s', $e['modtime'])); // lastmod_time + fwrite($fh, pack('s', $e['moddate'])); // lastmod_date + fwrite($fh, pack('V', $e['crc_32'])); // crc-32 + fwrite($fh, pack('I', $e['comsize'])); // compressed_size + fwrite($fh, pack('I', $e['uncsize'])); // uncompressed_size + fwrite($fh, pack('s', mb_strlen($cfilename, '8bit'))); // file_name_length + /** @noinspection DisconnectedForeachInstructionInspection */ + fwrite($fh, pack('s', 0)); // extra_field_length + fwrite($fh, $cfilename); // file_name + // ignoring extra_field + fwrite($fh, $zdata); + + // Append it to central dir + $e['external_attributes'] = (substr($cfilename, -1) === '/'&&!$zdata)?16:32; // Directory or file name + $e['comments'] = ''; + + $cdrec .= $dirSignature; + $cdrec .= "\x0\x0"; // version made by + $cdrec .= pack('v', $e['vneeded']); // version needed to extract + $cdrec .= "\x0\x0"; // general bit flag + $cdrec .= pack('v', $e['cmethod']); // compression method + $cdrec .= pack('v', $e['modtime']); // lastmod time + $cdrec .= pack('v', $e['moddate']); // lastmod date + $cdrec .= pack('V', $e['crc_32']); // crc32 + $cdrec .= pack('V', $e['comsize']); // compressed filesize + $cdrec .= pack('V', $e['uncsize']); // uncompressed filesize + $cdrec .= pack('v', mb_strlen($cfilename,'8bit')); // file name length + $cdrec .= pack('v', 0); // extra field length + $cdrec .= pack('v', mb_strlen($e['comments'],'8bit')); // file comment length + $cdrec .= pack('v', 0); // disk number start + $cdrec .= pack('v', 0); // internal file attributes + $cdrec .= pack('V', $e['external_attributes']); // internal file attributes + $cdrec .= pack('V', $e['offset']); // relative offset of local header + $cdrec .= $cfilename; + $cdrec .= $e['comments']; + } + + private function worksheetEncode(&$sheet, &$data) { + $COLS = []; $ROWS = []; - if ( count($this->rows) ) { + if ( count($sheet['rows']) ) { $CUR_ROW = 0; $COL = []; - foreach( $this->rows as $r ) { + foreach( $sheet['rows'] as $r ) { $CUR_ROW++; $row = ''; $CUR_COL = 0; @@ -208,12 +302,18 @@ class SimpleXLSXGen { } elseif ( preg_match('/^(\d\d\d\d)-(\d\d)-(\d\d)$/', $v, $m ) ){ $cv = $this->date2excel($m[1],$m[2],$m[3]); $cs = 4; // [14] mm-dd-yy + } elseif ( preg_match('/^(\d\d)\/(\d\d)\/(\d\d\d\d)$/', $v, $m ) ){ + $cv = $this->date2excel($m[3],$m[2],$m[1]); + $cs = 4; // [14] mm-dd-yy } elseif ( preg_match('/^(\d\d):(\d\d):(\d\d)$/', $v, $m ) ){ $cv = $this->date2excel(0,0,0,$m[1],$m[2],$m[3]); $cs = 5; // [14] mm-dd-yy } elseif ( preg_match('/^(\d\d\d\d)-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)$/', $v, $m ) ) { $cv = $this->date2excel( $m[1], $m[2], $m[3], $m[4], $m[5], $m[6] ); $cs = 6; // [22] m/d/yy h:mm + } elseif ( preg_match('/^(\d\d)\/(\d\d)\/(\d\d\d\d) (\d\d):(\d\d):(\d\d)$/', $v, $m ) ) { + $cv = $this->date2excel( $m[3], $m[2], $m[1], $m[4], $m[5], $m[6] ); + $cs = 6; // [22] m/d/yy h:mm } elseif ( mb_strlen( $v ) > 160 ) { $ct = 'inlineStr'; $cv = str_replace(['&','<','>',"\x03"],['&','<','>',''], $v); @@ -226,14 +326,14 @@ class SimpleXLSXGen { $v = str_replace(['&','<','>',"\x03"],['&','<','>',''], $v); $cv = false; $skey = '~'.$v; - if ( isset($SI_KEYS[ $skey ]) ) { - $cv = $SI_KEYS[ $skey ]; + if ( isset($this->SI_KEYS[ $skey ]) ) { + $cv = $this->SI_KEYS[ $skey ]; } if ( $cv === false ) { - $SI[] = $v; - $cv = count( $SI ) - 1; - $SI_KEYS[$skey] = $cv; + $this->SI[] = $v; + $cv = count( $this->SI ) - 1; + $this->SI_KEYS[$skey] = $cv; } } } elseif ( is_int( $v ) || is_float( $v ) ) { @@ -248,7 +348,6 @@ class SimpleXLSXGen { $ROWS[] = $row . "\r\n"; } foreach ( $COL as $k => $max ) { -// $COLS[] = ''; $COLS[] = ''; } $REF = 'A1:'.$this->num2name(count($COLS)) . $CUR_ROW; @@ -256,98 +355,82 @@ class SimpleXLSXGen { $COLS[] = ''; $ROWS[] = '0'; $REF = 'A1:A1'; - $SI[] = 'No Data'; } - $SI_CNT = count($SI); - $SI = ''.implode("\r\n", $SI).''; + return str_replace(['{REF}','{COLS}','{ROWS}'], + [ $REF, implode("\r\n", $COLS), implode("\r\n",$ROWS) ], + $data ); + } - $cdrec = ''; + private function _generate( $fh ) { + + + $dirSignatureE= "\x50\x4b\x05\x06"; // end of central dir signature + $zipComments = 'Generated by '.__CLASS__.' PHP class, thanks sergey.shuchkin@gmail.com'; + + if (!$fh) { + return false; + } + + $cdrec = ''; // central directory content + $entries= 0; // number of zipped files foreach ($this->template as $cfilename => $data ) { - - if ( $cfilename === 'docProps/core.xml' ) { + if ( $cfilename === '[Content_Types].xml' ) { + $s = ''; + for ($i=0; $i < count($this->sheets); $i++) { + $s .= ''; + } + $data = str_replace('{SHEETS}', $s, $data); + $this->_saveIt($fh, $cdrec, $cfilename, $data); + $entries++; + } + elseif ( $cfilename === 'xl/_rels/workbook.xml.rels' ) { + $s = ''; + for ($i=0; $i < count($this->sheets); $i++) { + $s .= '\n"; + } + $s .= ''; + $data = str_replace('{SHEETS}', $s, $data); + $this->_saveIt($fh, $cdrec, $cfilename, $data); + $entries++; + } + elseif ( $cfilename === 'xl/workbook.xml' ) { + $s = ''; + for ($i=0; $i < count($this->sheets); $i++) { + $s .= ''; + } + $data = str_replace('{SHEETS}', $s, $data); + $this->_saveIt($fh, $cdrec, $cfilename, $data); + $entries++; + } + elseif ( $cfilename === 'docProps/core.xml' ) { $data = str_replace('{DATE}', gmdate('Y-m-d\TH:i:s\Z'), $data); + $this->_saveIt($fh, $cdrec, $cfilename, $data); + $entries++; } elseif ( $cfilename === 'xl/sharedStrings.xml' ) { - $data = str_replace(['{CNT}', '{STRINGS}'], [ $SI_CNT, $SI ], $data ); + if (!count($this->SI)) { + $this->SI[] = 'No Data'; + } + $si_cnt = count($this->SI); + $this->SI = ''.implode("\r\n", $this->SI).''; + $data = str_replace(['{CNT}', '{STRINGS}'], [ $si_cnt, $this->SI ], $data ); + $this->_saveIt($fh, $cdrec, $cfilename, $data); + $entries++; } elseif ( $cfilename === 'xl/worksheets/sheet1.xml' ) { - $data = str_replace(['{REF}','{COLS}','{ROWS}'],[ $REF, implode("\r\n", $COLS), implode("\r\n",$ROWS) ], $data ); + for ($i=0; $i < count($this->sheets); $i++) { + $filename = 'xl/worksheets/sheet'.($i+1).'.xml'; + $data1 = $this->worksheetEncode($this->sheets[$i], $data); + $this->_saveIt($fh, $cdrec, $filename, $data1); + $entries++; + } + $data1=null; } - - $e = []; - $e['uncsize'] = mb_strlen($data, '8bit'); - - // if data to compress is too small, just store it - if($e['uncsize'] < 256){ - $e['comsize'] = $e['uncsize']; - $e['vneeded'] = 10; - $e['cmethod'] = 0; - $zdata = $data; - } else{ // otherwise, compress it - $zdata = gzcompress($data); - $zdata = substr(substr($zdata, 0, - 4 ), 2); // fix crc bug (thanks to Eric Mueller) - $e['comsize'] = mb_strlen($zdata, '8bit'); - $e['vneeded'] = 10; - $e['cmethod'] = 8; + else { + $this->_saveIt($fh, $cdrec, $cfilename, $data); + $entries++; } - - $e['bitflag'] = 0; - $e['crc_32'] = crc32($data); - - // Convert date and time to DOS Format, and set then - $lastmod_timeS = str_pad(decbin(date('s')>=32?date('s')-32:date('s')), 5, '0', STR_PAD_LEFT); - $lastmod_timeM = str_pad(decbin(date('i')), 6, '0', STR_PAD_LEFT); - $lastmod_timeH = str_pad(decbin(date('H')), 5, '0', STR_PAD_LEFT); - $lastmod_dateD = str_pad(decbin(date('d')), 5, '0', STR_PAD_LEFT); - $lastmod_dateM = str_pad(decbin(date('m')), 4, '0', STR_PAD_LEFT); - $lastmod_dateY = str_pad(decbin(date('Y')-1980), 7, '0', STR_PAD_LEFT); - - # echo "ModTime: $lastmod_timeS-$lastmod_timeM-$lastmod_timeH (".date("s H H").")\n"; - # echo "ModDate: $lastmod_dateD-$lastmod_dateM-$lastmod_dateY (".date("d m Y").")\n"; - $e['modtime'] = bindec("$lastmod_timeH$lastmod_timeM$lastmod_timeS"); - $e['moddate'] = bindec("$lastmod_dateY$lastmod_dateM$lastmod_dateD"); - - $e['offset'] = ftell($fh); - - /** @noinspection DisconnectedForeachInstructionInspection */ - fwrite($fh, $zipSignature); - fwrite($fh, pack('s', $e['vneeded'])); // version_needed - fwrite($fh, pack('s', $e['bitflag'])); // general_bit_flag - fwrite($fh, pack('s', $e['cmethod'])); // compression_method - fwrite($fh, pack('s', $e['modtime'])); // lastmod_time - fwrite($fh, pack('s', $e['moddate'])); // lastmod_date - fwrite($fh, pack('V', $e['crc_32'])); // crc-32 - fwrite($fh, pack('I', $e['comsize'])); // compressed_size - fwrite($fh, pack('I', $e['uncsize'])); // uncompressed_size - fwrite($fh, pack('s', mb_strlen($cfilename, '8bit'))); // file_name_length - /** @noinspection DisconnectedForeachInstructionInspection */ - fwrite($fh, pack('s', 0)); // extra_field_length - fwrite($fh, $cfilename); // file_name - // ignoring extra_field - fwrite($fh, $zdata); - - // Append it to central dir - $e['external_attributes'] = (substr($cfilename, -1) === '/'&&!$zdata)?16:32; // Directory or file name - $e['comments'] = ''; - - $cdrec .= $dirSignature; - $cdrec .= "\x0\x0"; // version made by - $cdrec .= pack('v', $e['vneeded']); // version needed to extract - $cdrec .= "\x0\x0"; // general bit flag - $cdrec .= pack('v', $e['cmethod']); // compression method - $cdrec .= pack('v', $e['modtime']); // lastmod time - $cdrec .= pack('v', $e['moddate']); // lastmod date - $cdrec .= pack('V', $e['crc_32']); // crc32 - $cdrec .= pack('V', $e['comsize']); // compressed filesize - $cdrec .= pack('V', $e['uncsize']); // uncompressed filesize - $cdrec .= pack('v', mb_strlen($cfilename,'8bit')); // file name length - $cdrec .= pack('v', 0); // extra field length - $cdrec .= pack('v', mb_strlen($e['comments'],'8bit')); // file comment length - $cdrec .= pack('v', 0); // disk number start - $cdrec .= pack('v', 0); // internal file attributes - $cdrec .= pack('V', $e['external_attributes']); // internal file attributes - $cdrec .= pack('V', $e['offset']); // relative offset of local header - $cdrec .= $cfilename; - $cdrec .= $e['comments']; } $before_cd = ftell($fh); fwrite($fh, $cdrec); @@ -356,8 +439,8 @@ class SimpleXLSXGen { fwrite($fh, $dirSignatureE); fwrite($fh, pack('v', 0)); // number of this disk fwrite($fh, pack('v', 0)); // number of the disk with the start of the central directory - fwrite($fh, pack('v', count( $this->template ))); // total # of entries "on this disk" - fwrite($fh, pack('v', count( $this->template ))); // total # of entries overall + fwrite($fh, pack('v', $entries)); // total # of entries "on this disk" + fwrite($fh, pack('v', $entries)); // total # of entries overall fwrite($fh, pack('V', mb_strlen($cdrec,'8bit'))); // size 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 @@ -365,6 +448,7 @@ class SimpleXLSXGen { return true; } + public function num2name($num) { $numeric = ($num - 1) % 26; $letter = chr( 65 + $numeric ); @@ -374,6 +458,7 @@ class SimpleXLSXGen { } return $letter; } + public function date2excel($year, $month, $day, $hours=0, $minutes=0, $seconds=0) { $excelTime = (($hours * 3600) + ($minutes * 60) + $seconds) / 86400;