From 83b44eca2751cbb1d91c3d350df6a829ca51d6aa Mon Sep 17 00:00:00 2001 From: Stefan Date: Mon, 6 Mar 2017 23:27:02 +0100 Subject: [PATCH] Initial cell autosizing commit - should not introduce breaking changes. --- .../Writer/XLSX/Helper/SizeCalculator.php | 69 +++++++++++ .../Writer/XLSX/Helper/SizeCollection.php | 88 ++++++++++++++ .../Writer/XLSX/Helper/size_collection.csv | 8 ++ src/Spout/Writer/XLSX/Internal/Workbook.php | 25 +++- src/Spout/Writer/XLSX/Internal/Worksheet.php | 107 +++++++++++++++--- src/Spout/Writer/XLSX/Writer.php | 32 +++++- .../Writer/XLSX/Helper/SizeCalculatorTest.php | 36 ++++++ .../Writer/XLSX/Helper/SizeCollectionTest.php | 31 +++++ tests/Spout/Writer/XLSX/WriterTest.php | 15 +++ 9 files changed, 391 insertions(+), 20 deletions(-) create mode 100644 src/Spout/Writer/XLSX/Helper/SizeCalculator.php create mode 100644 src/Spout/Writer/XLSX/Helper/SizeCollection.php create mode 100644 src/Spout/Writer/XLSX/Helper/size_collection.csv create mode 100644 tests/Spout/Writer/XLSX/Helper/SizeCalculatorTest.php create mode 100644 tests/Spout/Writer/XLSX/Helper/SizeCollectionTest.php diff --git a/src/Spout/Writer/XLSX/Helper/SizeCalculator.php b/src/Spout/Writer/XLSX/Helper/SizeCalculator.php new file mode 100644 index 0000000..9328cb8 --- /dev/null +++ b/src/Spout/Writer/XLSX/Helper/SizeCalculator.php @@ -0,0 +1,69 @@ +sizeCollection = $sizeCollection; + } + + /** + * Return the estimated width of a cell value. + * + * @param mixed $value + * @param int $fontSize + * @return float + */ + public function getCellWidth($value, $fontSize) + { + $width = 1; + foreach ($this->getSingleCharacterArray($value) as $character) { + if (isset($this->characterSizes[$character])) { + $width += $this->characterSizes[$character]; + } elseif (strlen($character)) { + $width += 0.1 * $fontSize; + } + } + + return $width; + } + + /** + * Set proper font sizes by font. + * + * @param string $fontName + * @param string $fontSize + */ + public function setFont($fontName, $fontSize) + { + $this->characterSizes = $this->sizeCollection->get($fontName, $fontSize); + } + + /** + * Split value into individual characters. + * + * @param mixed $value + * @return array + */ + private function getSingleCharacterArray($value) + { + if (mb_strlen($value) == strlen($value)) { + return str_split($value); + } + + return preg_split('~~u', $value, -1, PREG_SPLIT_NO_EMPTY); + } +} \ No newline at end of file diff --git a/src/Spout/Writer/XLSX/Helper/SizeCollection.php b/src/Spout/Writer/XLSX/Helper/SizeCollection.php new file mode 100644 index 0000000..be65886 --- /dev/null +++ b/src/Spout/Writer/XLSX/Helper/SizeCollection.php @@ -0,0 +1,88 @@ +addSizesToCollection($head, $row); + } + } + + /** + * Return character sizes for given font name. + * + * @param string $fontName + * @param int $fontSize + * @return array + */ + public function get($fontName, $fontSize) + { + if (isset($this->sizes[$fontName][$fontSize])) { + return $this->sizes[$fontName][$fontSize]; + } + + return $this->calculate($fontName, $fontSize); + } + + /** + * Calculate character widths based on font name and size. + * + * @param string $fontName + * @param int $fontSize + * @return array + */ + private function calculate($fontName, $fontSize) + { + foreach ($this->getBaseSizes($fontName) as $character => $size) { + $size = round($size / self::BASE_SIZE * $fontSize, 3); + $this->sizes[$fontName][$fontSize][$character] = $size; + } + return $this->sizes[$fontName][$fontSize]; + } + + /** + * Get character base widths by font name or default. + * + * @param string $fontName + * @return array + */ + private function getBaseSizes($fontName) + { + if (isset($this->sizes[$fontName])) { + return $this->sizes[$fontName][self::BASE_SIZE]; + } + return $this->sizes['Calibri'][self::BASE_SIZE]; + } + + /** + * Add character widths for a single font. + * + * @param array $keys + * @param array $values + */ + private function addSizesToCollection(array $keys, array $values) + { + $fontName = array_shift($values); + $fontSize = array_shift($values); + $this->sizes[$fontName][$fontSize] = array_combine($keys, $values); + } +} \ No newline at end of file diff --git a/src/Spout/Writer/XLSX/Helper/size_collection.csv b/src/Spout/Writer/XLSX/Helper/size_collection.csv new file mode 100644 index 0000000..78fbe93 --- /dev/null +++ b/src/Spout/Writer/XLSX/Helper/size_collection.csv @@ -0,0 +1,8 @@ +font,size,height," ",!,"""",#,$,%,&,',(,),*,+,",",-,.,/,0,1,2,3,4,5,6,7,8,9,:,;,<,=,>,?,@,A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z,[,"\\",],^,_,`,a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,{,|,},~,,€,,‚,ƒ,„,…,†,‡,ˆ,‰,Š,‹,Œ,,Ž,,,‘,’,“,”,•,–,—,˜,™,š,›,œ,,ž,Ÿ, ,¡,¢,£,¤,¥,¦,§,¨,©,ª,«,¬,­,®,¯,°,±,²,³,´,µ,¶,·,¸,¹,º,»,¼,½,¾,¿,À,Á,Â,Ã,Ä,Å,Æ,Ç,È,É,Ê,Ë,Ì,Í,Î,Ï,Ð,Ñ,Ò,Ó,Ô,Õ,Ö,×,Ø,Ù,Ú,Û,Ü,Ý,Þ,ß,à,á,â,ã,ä,å,æ,ç,è,é,ê,ë,ì,í,î,ï,ð,ñ,ò,ó,ô,õ,ö,÷,ø,ù,ú,û,ü,ý,þ,ÿ +Arial,12,16,0.559,0.547,0.795,1.180,1.180,1.801,1.429,0.435,0.683,0.683,0.807,1.180,0.547,0.683,0.547,0.571,1.180,1.019,1.180,1.180,1.180,1.180,1.180,1.180,1.180,1.180,0.547,0.547,1.180,1.180,1.180,1.180,2.050,1.441,1.404,1.540,1.528,1.404,1.292,1.553,1.528,0.547,1.043,1.416,1.168,1.665,1.528,1.553,1.404,1.553,1.540,1.416,1.304,1.528,1.429,1.925,1.429,1.429,1.304,0.559,1.068,0.559,1.056,1.205,0.671,1.180,1.168,1.056,1.168,1.180,0.571,1.168,1.155,0.534,0.559,1.043,0.534,1.665,1.155,1.180,1.168,1.168,0.683,1.056,0.571,1.155,1.056,1.553,1.056,1.056,1.056,0.683,0.534,0.683,1.180,1.503,1.193,1.503,0.547,1.180,0.683,2.025,1.180,1.180,0.696,2.050,1.416,0.683,2.037,1.503,1.304,1.503,1.503,0.534,0.547,0.683,0.683,0.795,1.193,2.050,0.696,2.012,1.056,0.683,1.925,1.503,1.056,1.429,0.509,0.658,1.180,1.180,1.180,1.193,0.534,1.180,0.683,1.553,0.807,1.155,1.180,0.683,1.553,1.205,0.795,1.180,0.696,0.696,0.671,1.155,1.180,0.658,0.683,0.671,0.807,1.155,1.689,1.689,1.689,1.280,1.441,1.441,1.441,1.441,1.441,1.441,2.050,1.540,1.404,1.404,1.404,1.404,0.559,0.559,0.584,0.571,1.553,1.528,1.553,1.553,1.553,1.553,1.553,1.168,1.553,1.528,1.528,1.528,1.528,1.429,1.404,1.292,1.180,1.180,1.180,1.180,1.180,1.180,1.801,1.056,1.180,1.180,1.180,1.180,0.559,0.559,0.584,0.571,1.180,1.155,1.180,1.180,1.180,1.180,1.180,1.180,1.280,1.155,1.155,1.155,1.155,1.056,1.168,1.056 +Calibri,12,14,0.559,0.634,0.783,1.056,1.031,1.416,1.404,0.522,0.658,0.658,1.019,1.043,0.534,0.658,0.522,0.807,1.031,1.019,1.031,1.031,1.031,1.031,1.031,1.031,1.031,1.031,0.522,0.534,1.031,1.043,1.031,0.907,1.776,1.180,1.155,1.168,1.280,1.031,0.907,1.280,1.280,0.522,0.671,1.031,0.919,1.652,1.280,1.404,1.031,1.416,1.155,0.907,1.043,1.280,1.180,1.764,1.056,1.043,1.043,0.658,1.553,0.658,1.043,1.056,0.658,1.031,1.031,0.907,1.031,1.031,0.671,1.043,1.031,0.522,0.547,0.907,0.522,1.516,1.031,1.031,1.031,1.031,0.671,0.907,0.671,1.031,0.907,1.416,0.907,0.907,0.783,0.671,0.857,0.671,1.043,1.056,1.056,1.056,0.534,0.708,0.907,1.404,1.019,1.019,0.795,2.161,0.907,0.658,1.776,1.056,1.043,1.056,1.056,0.534,0.534,0.907,0.907,1.019,1.056,1.776,0.907,1.404,0.907,0.658,1.776,1.056,0.795,1.043,0.509,0.634,1.031,1.031,1.056,1.043,0.969,1.031,0.770,1.652,0.783,1.031,1.043,0.658,1.031,0.783,0.671,1.043,0.671,0.671,0.658,1.155,1.155,0.522,0.658,0.547,0.907,1.031,1.292,1.416,1.416,0.907,1.180,1.180,1.180,1.180,1.180,1.180,1.540,1.168,1.031,1.031,1.031,1.031,0.534,0.534,0.559,0.559,1.292,1.280,1.404,1.404,1.404,1.404,1.404,1.043,1.404,1.280,1.280,1.280,1.280,1.043,1.031,1.031,1.031,1.031,1.031,1.031,1.031,1.031,1.528,0.907,1.031,1.031,1.031,1.031,0.547,0.547,0.571,0.547,1.031,1.031,1.031,1.031,1.031,1.031,1.031,1.043,1.031,1.031,1.031,1.031,1.031,0.907,1.031,0.907 +Georgia,12,15,0.559,0.658,0.919,1.292,1.280,1.677,1.441,0.435,0.807,0.807,1.043,1.292,0.559,0.807,0.547,1.043,1.304,0.919,1.180,1.180,1.180,1.056,1.180,1.068,1.292,1.180,0.658,0.658,1.280,1.292,1.280,1.043,1.901,1.453,1.304,1.317,1.553,1.317,1.304,1.553,1.677,0.807,1.056,1.441,1.304,1.925,1.565,1.553,1.304,1.553,1.441,1.180,1.304,1.553,1.441,2.062,1.441,1.329,1.304,0.795,2.050,0.795,1.280,1.329,1.006,1.056,1.193,0.944,1.180,1.056,0.708,1.056,1.180,0.683,0.696,1.180,0.683,1.801,1.193,1.180,1.180,1.193,0.932,0.932,0.807,1.180,1.081,1.578,1.056,1.081,0.932,0.932,0.758,0.932,1.292,2.000,1.317,2.000,0.559,1.093,0.919,1.652,1.043,1.043,1.031,2.422,1.180,0.907,2.050,2.000,1.304,2.000,2.000,0.547,0.559,0.919,0.919,0.795,1.304,1.789,1.031,1.925,0.932,0.907,1.677,2.000,0.932,1.329,0.509,0.658,1.168,1.304,1.155,1.329,0.758,1.056,1.031,1.925,1.043,1.155,1.292,0.807,1.925,1.329,0.919,1.292,1.043,1.031,1.006,1.180,1.056,0.547,1.006,1.031,1.056,1.155,2.161,2.174,2.161,1.043,1.453,1.453,1.453,1.453,1.453,1.453,2.062,1.317,1.317,1.317,1.317,1.317,0.807,0.807,0.807,0.807,1.553,1.565,1.553,1.553,1.553,1.553,1.553,1.280,1.553,1.553,1.553,1.553,1.553,1.329,1.304,1.180,1.056,1.056,1.056,1.056,1.056,1.056,1.553,0.944,1.056,1.056,1.056,1.056,0.683,0.683,0.696,0.696,1.168,1.193,1.180,1.180,1.180,1.180,1.180,1.292,1.180,1.180,1.180,1.180,1.180,1.081,1.193,1.081 +"Segoe UI",12,16,0.559,0.658,0.795,1.193,1.155,1.677,1.677,0.534,0.671,0.671,0.932,1.404,0.435,0.795,0.435,0.832,1.168,1.155,1.168,1.155,1.180,1.155,1.168,1.168,1.168,1.168,0.435,0.435,1.379,1.404,1.379,0.919,1.901,1.317,1.168,1.304,1.416,1.043,1.043,1.416,1.404,0.559,0.795,1.180,1.043,1.776,1.528,1.553,1.168,1.553,1.292,1.168,1.068,1.404,1.304,1.925,1.193,1.180,1.180,0.671,1.578,0.671,1.404,0.932,0.559,1.056,1.168,0.932,1.180,1.056,0.696,1.180,1.155,0.534,0.571,1.043,0.534,1.776,1.155,1.180,1.168,1.180,0.795,0.932,0.696,1.155,1.056,1.553,0.944,1.056,0.932,0.683,0.534,0.683,1.404,1.292,1.168,1.292,0.547,1.168,0.807,1.528,0.807,0.807,0.807,2.422,1.168,0.683,1.925,1.292,1.180,1.292,1.292,0.435,0.435,0.807,0.807,0.907,1.056,2.050,0.696,1.540,0.932,0.683,1.925,1.292,0.932,1.180,0.509,0.658,1.155,1.168,1.180,1.180,0.534,0.919,0.919,1.776,0.807,1.056,1.404,0.795,1.776,0.932,0.807,1.404,0.807,0.795,0.671,1.168,0.932,0.435,0.447,0.783,0.932,1.056,1.901,1.913,1.913,0.919,1.317,1.317,1.317,1.317,1.317,1.317,1.801,1.304,1.043,1.043,1.043,1.043,0.559,0.571,0.584,0.584,1.429,1.528,1.553,1.553,1.553,1.553,1.553,1.379,1.553,1.404,1.404,1.404,1.404,1.180,1.168,1.168,1.056,1.056,1.056,1.056,1.056,1.056,1.677,0.932,1.056,1.056,1.056,1.056,0.559,0.559,0.584,0.584,1.180,1.155,1.180,1.180,1.180,1.180,1.180,1.404,1.193,1.155,1.155,1.155,1.155,1.056,1.168,1.056 +Tahoma,12,16,0.683,0.658,0.807,1.528,1.168,2.037,1.441,0.435,0.807,0.807,1.168,1.528,0.671,0.807,0.658,0.820,1.180,1.155,1.180,1.168,1.180,1.168,1.180,1.180,1.180,1.180,0.770,0.795,1.516,1.528,1.528,1.056,1.901,1.317,1.180,1.304,1.416,1.168,1.056,1.416,1.404,0.807,0.919,1.180,1.043,1.540,1.404,1.429,1.168,1.429,1.304,1.180,1.205,1.404,1.317,1.814,1.193,1.205,1.180,0.795,1.565,0.795,1.528,1.193,1.130,1.056,1.168,0.944,1.168,1.056,0.696,1.168,1.155,0.534,0.683,1.056,0.534,1.665,1.155,1.180,1.168,1.168,0.795,0.932,0.696,1.155,1.056,1.553,1.056,1.056,0.932,1.043,0.758,1.056,1.528,2.000,1.193,2.000,0.460,1.193,0.832,1.652,1.155,1.155,1.155,2.795,1.180,0.795,2.050,2.000,1.180,2.000,2.000,0.447,0.447,0.820,0.820,0.932,1.193,1.938,1.155,1.789,0.932,0.795,1.925,2.000,0.932,1.205,0.621,0.658,1.168,1.180,1.180,1.180,0.758,1.168,1.143,1.925,1.043,1.180,1.528,0.807,1.925,1.193,1.043,1.528,1.031,1.031,1.118,1.168,1.168,0.770,1.130,1.031,1.056,1.168,2.025,2.037,2.025,1.043,1.317,1.317,1.317,1.317,1.317,1.317,1.938,1.304,1.168,1.168,1.168,1.168,0.807,0.807,0.820,0.807,1.429,1.404,1.429,1.429,1.429,1.429,1.429,1.528,1.429,1.404,1.404,1.404,1.404,1.205,1.168,1.168,1.056,1.056,1.056,1.056,1.056,1.056,1.801,0.944,1.056,1.056,1.056,1.056,0.571,0.559,0.584,0.571,1.180,1.155,1.180,1.180,1.180,1.180,1.180,1.528,1.180,1.155,1.155,1.155,1.155,1.056,1.168,1.056 +"Times New Roman",12,16,0.559,0.658,0.907,1.056,1.056,1.677,1.553,0.435,0.683,0.683,1.031,1.180,0.559,0.683,0.534,0.571,1.056,1.031,1.056,1.043,1.056,1.043,1.056,1.056,1.056,1.056,0.547,0.547,1.180,1.180,1.180,0.932,1.925,1.553,1.416,1.429,1.540,1.304,1.180,1.553,1.553,0.683,0.820,1.553,1.304,1.801,1.565,1.540,1.180,1.540,1.429,1.168,1.304,1.553,1.553,1.925,1.553,1.553,1.304,0.671,1.068,0.683,1.056,1.081,0.671,0.944,1.068,0.932,1.068,0.932,0.708,1.056,1.056,0.571,0.584,1.068,0.571,1.565,1.056,1.056,1.068,1.056,0.696,0.807,0.571,1.056,1.056,1.553,1.056,1.056,0.932,1.019,0.410,1.019,1.180,1.516,1.068,1.516,0.671,1.056,0.932,2.025,1.056,1.031,0.683,2.050,1.168,0.683,1.801,1.516,1.304,1.516,1.516,0.547,0.547,0.932,0.932,0.795,1.081,2.075,0.696,2.050,0.807,0.683,1.553,1.516,0.932,1.553,0.509,0.658,1.043,1.056,1.056,1.056,0.410,1.031,0.683,1.553,0.584,1.056,1.180,0.683,1.553,1.081,0.807,1.180,0.683,0.683,0.671,1.168,0.957,0.658,0.658,0.658,0.683,1.056,1.540,1.540,1.553,0.932,1.553,1.553,1.553,1.553,1.553,1.553,1.814,1.429,1.304,1.304,1.304,1.304,0.683,0.683,0.683,0.683,1.540,1.565,1.540,1.540,1.540,1.540,1.540,1.155,1.540,1.553,1.553,1.553,1.553,1.553,1.180,1.056,0.944,0.944,0.944,0.944,0.944,0.944,1.429,0.932,0.932,0.932,0.932,0.932,0.571,0.571,0.571,0.571,1.056,1.056,1.056,1.056,1.056,1.056,1.056,1.180,1.056,1.056,1.056,1.056,1.056,1.056,1.068,1.056 +Verdana,12,15,0.807,0.770,0.919,1.652,1.292,2.161,1.565,0.547,0.919,0.907,1.280,1.652,0.783,0.919,0.770,0.944,1.292,1.267,1.292,1.280,1.304,1.292,1.292,1.292,1.304,1.304,0.882,0.907,1.640,1.652,1.640,1.155,2.025,1.429,1.416,1.429,1.540,1.292,1.168,1.553,1.528,0.907,0.919,1.416,1.168,1.652,1.528,1.665,1.292,1.665,1.429,1.416,1.304,1.528,1.429,2.050,1.429,1.304,1.429,0.907,1.814,0.907,1.652,1.329,1.230,1.292,1.292,1.056,1.292,1.292,0.820,1.292,1.280,0.547,0.807,1.180,0.534,2.025,1.280,1.292,1.292,1.292,0.919,1.056,0.807,1.280,1.180,1.677,1.180,1.180,1.056,1.280,0.882,1.280,1.652,2.000,1.304,2.000,0.571,1.317,0.944,1.652,1.280,1.280,1.255,3.031,1.416,0.907,2.174,2.000,1.429,2.000,2.000,0.571,0.559,0.944,0.932,1.155,1.280,2.025,1.280,2.012,1.056,0.907,2.037,2.000,1.056,1.304,0.733,0.770,1.280,1.292,1.292,1.304,0.882,1.280,1.255,2.025,1.155,1.280,1.652,0.919,2.025,1.329,1.155,1.652,1.155,1.155,1.230,1.280,1.280,0.770,1.242,1.143,1.168,1.292,2.025,2.037,2.037,1.168,1.429,1.429,1.429,1.429,1.429,1.429,2.037,1.429,1.292,1.292,1.292,1.292,0.919,0.907,0.919,0.919,1.553,1.528,1.665,1.665,1.665,1.665,1.665,1.627,1.665,1.528,1.528,1.528,1.528,1.304,1.292,1.292,1.292,1.292,1.292,1.292,1.292,1.292,1.925,1.056,1.292,1.292,1.292,1.292,0.559,0.559,0.584,0.571,1.292,1.280,1.292,1.292,1.292,1.292,1.292,1.652,1.292,1.280,1.280,1.280,1.280,1.180,1.292,1.180 diff --git a/src/Spout/Writer/XLSX/Internal/Workbook.php b/src/Spout/Writer/XLSX/Internal/Workbook.php index bdf027f..73422c9 100644 --- a/src/Spout/Writer/XLSX/Internal/Workbook.php +++ b/src/Spout/Writer/XLSX/Internal/Workbook.php @@ -5,6 +5,8 @@ namespace Box\Spout\Writer\XLSX\Internal; use Box\Spout\Writer\Common\Internal\AbstractWorkbook; use Box\Spout\Writer\XLSX\Helper\FileSystemHelper; use Box\Spout\Writer\XLSX\Helper\SharedStringsHelper; +use Box\Spout\Writer\XLSX\Helper\SizeCalculator; +use Box\Spout\Writer\XLSX\Helper\SizeCollection; use Box\Spout\Writer\XLSX\Helper\StyleHelper; use Box\Spout\Writer\Common\Sheet; @@ -26,6 +28,9 @@ class Workbook extends AbstractWorkbook /** @var bool Whether inline or shared strings should be used */ protected $shouldUseInlineStrings; + /** @var bool Determine whether cell widths and heights should be calculated */ + protected $shouldUseCellAutosizing; + /** @var \Box\Spout\Writer\XLSX\Helper\FileSystemHelper Helper to perform file system operations */ protected $fileSystemHelper; @@ -35,23 +40,29 @@ class Workbook extends AbstractWorkbook /** @var \Box\Spout\Writer\XLSX\Helper\StyleHelper Helper to apply styles */ protected $styleHelper; + /** @var \Box\Spout\Writer\XLSX\Helper\SizeCalculator To calculate cell sizes */ + protected $sizeCalculator; + /** * @param string $tempFolder * @param bool $shouldUseInlineStrings + * @param bool $shouldUseCellAutosizing * @param bool $shouldCreateNewSheetsAutomatically * @param \Box\Spout\Writer\Style\Style $defaultRowStyle * @throws \Box\Spout\Common\Exception\IOException If unable to create at least one of the base folders */ - public function __construct($tempFolder, $shouldUseInlineStrings, $shouldCreateNewSheetsAutomatically, $defaultRowStyle) + public function __construct($tempFolder, $shouldUseInlineStrings, $shouldUseCellAutosizing, $shouldCreateNewSheetsAutomatically, $defaultRowStyle) { parent::__construct($shouldCreateNewSheetsAutomatically, $defaultRowStyle); $this->shouldUseInlineStrings = $shouldUseInlineStrings; + $this->shouldUseCellAutosizing = $shouldUseCellAutosizing; $this->fileSystemHelper = new FileSystemHelper($tempFolder); $this->fileSystemHelper->createBaseFilesAndFolders(); $this->styleHelper = new StyleHelper($defaultRowStyle); + $this->sizeCalculator = new SizeCalculator(new SizeCollection()); // This helper will be shared by all sheets $xlFolder = $this->fileSystemHelper->getXlFolder(); @@ -86,7 +97,15 @@ class Workbook extends AbstractWorkbook $sheet = new Sheet($newSheetIndex); $worksheetFilesFolder = $this->fileSystemHelper->getXlWorksheetsFolder(); - $worksheet = new Worksheet($sheet, $worksheetFilesFolder, $this->sharedStringsHelper, $this->styleHelper, $this->shouldUseInlineStrings); + $worksheet = new Worksheet( + $sheet, + $worksheetFilesFolder, + $this->sharedStringsHelper, + $this->styleHelper, + $this->sizeCalculator, + $this->shouldUseInlineStrings, + $this->shouldUseCellAutosizing + ); $this->worksheets[] = $worksheet; return $worksheet; @@ -99,6 +118,7 @@ class Workbook extends AbstractWorkbook * * @param resource $finalFilePointer Pointer to the XLSX that will be created * @return void + * @throws \Box\Spout\Common\Exception\IOException */ public function close($finalFilePointer) { @@ -126,6 +146,7 @@ class Workbook extends AbstractWorkbook * Deletes the root folder created in the temp folder and all its contents. * * @return void + * @throws \Box\Spout\Common\Exception\IOException */ protected function cleanupTempFolder() { diff --git a/src/Spout/Writer/XLSX/Internal/Worksheet.php b/src/Spout/Writer/XLSX/Internal/Worksheet.php index b5a3dc7..5e25842 100644 --- a/src/Spout/Writer/XLSX/Internal/Worksheet.php +++ b/src/Spout/Writer/XLSX/Internal/Worksheet.php @@ -3,10 +3,12 @@ namespace Box\Spout\Writer\XLSX\Internal; use Box\Spout\Common\Exception\InvalidArgumentException; +use Box\Spout\Writer\Common\Internal\WorksheetInterface; +use Box\Spout\Writer\XLSX\Helper\SizeCalculator; +use Box\Spout\Writer\Common\Helper\CellHelper; use Box\Spout\Common\Exception\IOException; use Box\Spout\Common\Helper\StringHelper; -use Box\Spout\Writer\Common\Helper\CellHelper; -use Box\Spout\Writer\Common\Internal\WorksheetInterface; +use Box\Spout\Writer\Style\Style; /** * Class Worksheet @@ -45,32 +47,52 @@ EOD; /** @var bool Whether inline or shared strings should be used */ protected $shouldUseInlineStrings; + /** @var bool Determine whether cell widths should be calculated */ + protected $shouldUseCellAutosizing; + /** @var \Box\Spout\Common\Escaper\XLSX Strings escaper */ protected $stringsEscaper; /** @var \Box\Spout\Common\Helper\StringHelper String helper */ protected $stringHelper; + /** @var \Box\Spout\Writer\XLSX\Helper\SizeCalculator */ + protected $sizeCalculator; + /** @var Resource Pointer to the sheet data file (e.g. xl/worksheets/sheet1.xml) */ protected $sheetFilePointer; /** @var int Index of the last written row */ protected $lastWrittenRowIndex = 0; + /** @var array Holds the column widths for cell sizing */ + protected $columnWidths = []; + /** * @param \Box\Spout\Writer\Common\Sheet $externalSheet The associated "external" sheet * @param string $worksheetFilesFolder Temporary folder where the files to create the XLSX will be stored * @param \Box\Spout\Writer\XLSX\Helper\SharedStringsHelper $sharedStringsHelper Helper for shared strings * @param \Box\Spout\Writer\XLSX\Helper\StyleHelper Helper to work with styles + * @param \Box\Spout\Writer\XLSX\Helper\SizeCalculator $sizeCalculator To calculate cell sizes * @param bool $shouldUseInlineStrings Whether inline or shared strings should be used + * @param bool $shouldUseCellAutosizing Whether cell sizes should be calculated or not * @throws \Box\Spout\Common\Exception\IOException If the sheet data file cannot be opened for writing */ - public function __construct($externalSheet, $worksheetFilesFolder, $sharedStringsHelper, $styleHelper, $shouldUseInlineStrings) - { + public function __construct( + $externalSheet, + $worksheetFilesFolder, + $sharedStringsHelper, + $styleHelper, + $sizeCalculator, + $shouldUseInlineStrings, + $shouldUseCellAutosizing + ) { $this->externalSheet = $externalSheet; $this->sharedStringsHelper = $sharedStringsHelper; $this->styleHelper = $styleHelper; + $this->sizeCalculator = $sizeCalculator; $this->shouldUseInlineStrings = $shouldUseInlineStrings; + $this->shouldUseCellAutosizing = $shouldUseCellAutosizing; /** @noinspection PhpUnnecessaryFullyQualifiedNameInspection */ $this->stringsEscaper = \Box\Spout\Common\Escaper\XLSX::getInstance(); @@ -81,7 +103,8 @@ EOD; } /** - * Prepares the worksheet to accept data + * Prepares the worksheet to accept data and preserves free space at the beginning + * of the sheet file to prepend header xml and optional column size data. * * @return void * @throws \Box\Spout\Common\Exception\IOException If the sheet data file cannot be opened for writing @@ -91,7 +114,8 @@ EOD; $this->sheetFilePointer = fopen($this->worksheetFilePath, 'w'); $this->throwIfSheetFilePointerIsNotAvailable(); - fwrite($this->sheetFilePointer, self::SHEET_XML_FILE_HEADER); + $spaceToPreserve = $this->shouldUseCellAutosizing ? 1024 * 1024 : 512; + fwrite($this->sheetFilePointer, str_repeat(' ', $spaceToPreserve)); fwrite($this->sheetFilePointer, ''); } @@ -184,8 +208,12 @@ EOD; $rowXML = ''; + if ($this->shouldUseCellAutosizing) { + $this->sizeCalculator->setFont($style->getFontName(), $style->getFontSize()); + } + foreach($dataRow as $cellValue) { - $rowXML .= $this->getCellXML($rowIndex, $cellNumber, $cellValue, $style->getId()); + $rowXML .= $this->getCellXML($rowIndex, $cellNumber, $cellValue, $style); $cellNumber++; } @@ -200,18 +228,19 @@ EOD; /** * Build and return xml for a single cell. * - * @param int $rowIndex - * @param int $cellNumber + * @param int $rowIndex + * @param int $cellNumber * @param mixed $cellValue - * @param int $styleId + * @param Style $style Style to be applied to the row. NULL means use default style. + * * @return string * @throws InvalidArgumentException If the given value cannot be processed */ - private function getCellXML($rowIndex, $cellNumber, $cellValue, $styleId) + private function getCellXML($rowIndex, $cellNumber, $cellValue, Style $style) { $columnIndex = CellHelper::getCellIndexFromColumnIndex($cellNumber); $cellXML = 'getId() . '"'; if (CellHelper::isNonEmptyString($cellValue)) { $cellXML .= $this->getCellXMLFragmentForNonEmptyString($cellValue); @@ -220,7 +249,7 @@ EOD; } else if (CellHelper::isNumeric($cellValue)) { $cellXML .= '>' . $cellValue . ''; } else if (empty($cellValue)) { - if ($this->styleHelper->shouldApplyStyleOnEmptyCell($styleId)) { + if ($this->styleHelper->shouldApplyStyleOnEmptyCell($style->getId())) { $cellXML .= '/>'; } else { // don't write empty cells that do no need styling @@ -231,6 +260,8 @@ EOD; throw new InvalidArgumentException('Trying to add a value with an unsupported type: ' . gettype($cellValue)); } + $this->updateColumnWidth($cellNumber, $cellValue, $style); + return $cellXML; } @@ -257,6 +288,48 @@ EOD; return $cellXMLFragment; } + /** + * Update the width of the current cellNumber, if cell autosizing is enabled + * and the width of the current value exceeds a previously calculated one. + * + * @param int $cellNumber + * @param string $cellValue + * @param Style $style + */ + private function updateColumnWidth($cellNumber, $cellValue, $style) + { + if ($this->shouldUseCellAutosizing) { + $cellWidth = $this->sizeCalculator->getCellWidth($cellValue, $style->getFontSize()); + if (!isset($this->columnWidths[$cellNumber]) || $cellWidth > $this->columnWidths[$cellNumber]) { + $this->columnWidths[$cellNumber] = $cellWidth; + } + } + } + + /** + * Return writable xml string, if column widths have been + * calculated or custom widths have been set. + * + * @return string + */ + private function getColsXML() + { + if (0 === count($this->columnWidths)) { + return ''; + } + + $colsXml = ''; + $colTemplate = ''; + + foreach ($this->columnWidths as $columnIndex => $columnWidth) { + $colsXml .= sprintf($colTemplate, $columnIndex+1, $columnIndex+1, $columnWidth); + } + + $colsXml .= ''; + + return $colsXml; + } + /** * Closes the worksheet * @@ -268,8 +341,12 @@ EOD; return; } - fwrite($this->sheetFilePointer, ''); - fwrite($this->sheetFilePointer, ''); + fwrite($this->sheetFilePointer, ''); + rewind($this->sheetFilePointer); + + fwrite($this->sheetFilePointer, self::SHEET_XML_FILE_HEADER); + fwrite($this->sheetFilePointer, $this->getColsXML()); + fclose($this->sheetFilePointer); } } diff --git a/src/Spout/Writer/XLSX/Writer.php b/src/Spout/Writer/XLSX/Writer.php index 965955a..1d4376e 100644 --- a/src/Spout/Writer/XLSX/Writer.php +++ b/src/Spout/Writer/XLSX/Writer.php @@ -3,8 +3,8 @@ namespace Box\Spout\Writer\XLSX; use Box\Spout\Writer\AbstractMultiSheetsWriter; -use Box\Spout\Writer\Style\StyleBuilder; use Box\Spout\Writer\XLSX\Internal\Workbook; +use Box\Spout\Writer\Style\StyleBuilder; /** * Class Writer @@ -27,6 +27,9 @@ class Writer extends AbstractMultiSheetsWriter /** @var bool Whether inline or shared strings should be used - inline string is more memory efficient */ protected $shouldUseInlineStrings = true; + /** @var bool Determine whether cell widths should be calculated */ + protected $shouldUseCellAutosizing = false; + /** @var Internal\Workbook The workbook for the XLSX file */ protected $book; @@ -64,6 +67,22 @@ class Writer extends AbstractMultiSheetsWriter return $this; } + /** + * Enable or disable automated calculation of cell sizes to fit the contents of a cell value. + * + * @api + * @param bool $shouldUseCellAutosizing + * @return Writer + * @throws \Box\Spout\Writer\Exception\WriterAlreadyOpenedException If the writer was already opened + */ + public function setShouldUseCellAutosizing($shouldUseCellAutosizing) + { + $this->throwIfWriterAlreadyOpened('Writer must be configured before opening it.'); + + $this->shouldUseCellAutosizing = $shouldUseCellAutosizing; + return $this; + } + /** * Configures the write and sets the current sheet pointer to a new sheet. * @@ -74,7 +93,13 @@ class Writer extends AbstractMultiSheetsWriter { if (!$this->book) { $tempFolder = ($this->tempFolder) ? : sys_get_temp_dir(); - $this->book = new Workbook($tempFolder, $this->shouldUseInlineStrings, $this->shouldCreateNewSheetsAutomatically, $this->defaultRowStyle); + $this->book = new Workbook( + $tempFolder, + $this->shouldUseInlineStrings, + $this->shouldUseCellAutosizing, + $this->shouldCreateNewSheetsAutomatically, + $this->defaultRowStyle + ); $this->book->addNewSheetAndMakeItCurrent(); } } @@ -98,6 +123,7 @@ class Writer extends AbstractMultiSheetsWriter * @return void * @throws \Box\Spout\Writer\Exception\WriterNotOpenedException If the book is not created yet * @throws \Box\Spout\Common\Exception\IOException If unable to write data + * @throws \Box\Spout\Writer\Exception\WriterException */ protected function addRowToWriter(array $dataRow, $style) { @@ -120,8 +146,8 @@ class Writer extends AbstractMultiSheetsWriter /** * Closes the writer, preventing any additional writing. - * * @return void + * @throws \Box\Spout\Common\Exception\IOException */ protected function closeWriter() { diff --git a/tests/Spout/Writer/XLSX/Helper/SizeCalculatorTest.php b/tests/Spout/Writer/XLSX/Helper/SizeCalculatorTest.php new file mode 100644 index 0000000..7fdcdd1 --- /dev/null +++ b/tests/Spout/Writer/XLSX/Helper/SizeCalculatorTest.php @@ -0,0 +1,36 @@ +getSizeCollectionMock(); + $sizeCollectionMock->expects(self::once())->method('get')->with($fontName, $fontSize); + + $sizeCalculator = new SizeCalculator($sizeCollectionMock); + $sizeCalculator->setFont($fontName, $fontSize); + } + + public function testGetCellWidthShouldReturnValueGreaterThanOneForNonEmptyString() + { + $sizeCalculator = new SizeCalculator($this->getSizeCollectionMock()); + self::assertGreaterThan(1, $sizeCalculator->getCellWidth('a', 12)); + } + + /** + * @return \PHPUnit_Framework_MockObject_MockObject|SizeCollection + */ + private function getSizeCollectionMock() + { + return $this->getMockBuilder('Box\Spout\Writer\XLSX\Helper\SizeCollection')->disableOriginalConstructor()->getMock(); + } +} diff --git a/tests/Spout/Writer/XLSX/Helper/SizeCollectionTest.php b/tests/Spout/Writer/XLSX/Helper/SizeCollectionTest.php new file mode 100644 index 0000000..35d2b4f --- /dev/null +++ b/tests/Spout/Writer/XLSX/Helper/SizeCollectionTest.php @@ -0,0 +1,31 @@ +get('Arial', '12'))); + } + + public function testGetWithBiggerFontSizeShouldReturnArrayWithGreaterSum() + { + $collection = new SizeCollection(); + self::assertGreaterThan( + array_sum($collection->get('Arial', '12')), + array_sum($collection->get('Arial', '13')) + ); + } + + public function testNonExistingFontShouldStillReturnArrayWithSizes() + { + $collection = new SizeCollection(); + self::assertGreaterThan(200, count($collection->get('MeNoFont', '12'))); + } +} diff --git a/tests/Spout/Writer/XLSX/WriterTest.php b/tests/Spout/Writer/XLSX/WriterTest.php index 562db8d..5af5421 100644 --- a/tests/Spout/Writer/XLSX/WriterTest.php +++ b/tests/Spout/Writer/XLSX/WriterTest.php @@ -78,6 +78,21 @@ class WriterTest extends \PHPUnit_Framework_TestCase $writer->setShouldUseInlineStrings(true); } + /** + * @expectedException \Box\Spout\Writer\Exception\WriterAlreadyOpenedException + */ + public function testSetShouldUseCellAutosizingShouldThrowExceptionIfCalledAfterOpeningWriter() + { + $fileName = 'file_that_wont_be_written.xlsx'; + $filePath = $this->getGeneratedResourcePath($fileName); + + /** @var \Box\Spout\Writer\XLSX\Writer $writer */ + $writer = WriterFactory::create(Type::XLSX); + $writer->openToFile($filePath); + + $writer->setShouldUseCellAutosizing(true); + } + /** * @expectedException \Box\Spout\Writer\Exception\WriterAlreadyOpenedException */