From 51a75c8e25066a930708b2f4424a22db82aab16f Mon Sep 17 00:00:00 2001 From: Oliver Vogel Date: Tue, 18 Mar 2014 18:08:04 +0100 Subject: [PATCH] text reorganization --- .../Exception/FontNotApplicableException.php | 8 + .../Image/Exception/FontNotFoundException.php | 8 + src/Intervention/Image/Font.php | 472 ++++++++++++++++++ src/Intervention/Image/Image.php | 65 ++- tests/FontTest.php | 345 +++++++++++++ tests/ImageTest.php | 36 +- 6 files changed, 931 insertions(+), 3 deletions(-) create mode 100644 src/Intervention/Image/Exception/FontNotApplicableException.php create mode 100644 src/Intervention/Image/Exception/FontNotFoundException.php create mode 100644 src/Intervention/Image/Font.php create mode 100644 tests/FontTest.php diff --git a/src/Intervention/Image/Exception/FontNotApplicableException.php b/src/Intervention/Image/Exception/FontNotApplicableException.php new file mode 100644 index 00000000..6dca66e1 --- /dev/null +++ b/src/Intervention/Image/Exception/FontNotApplicableException.php @@ -0,0 +1,8 @@ +text = $text; + } + + /** + * Set text to be written + * + * @param String $text + * @return void + */ + public function text($text) + { + $this->text = $text; + } + + /** + * Get text to be written + * + * @return String + */ + public function getText() + { + return $this->text; + } + + /** + * Set font size in pixels + * + * @param integer $size + * @return void + */ + public function size($size) + { + $this->size = $size; + } + + /** + * Get font size in pixels + * + * @return integer + */ + public function getSize() + { + return $this->size; + } + + /** + * Get font size in points + * + * @return integer + */ + public function getPointSize() + { + return intval(ceil($this->size * 0.75)); + } + + /** + * Set color of text to be written + * + * @param mixed $color + * @return void + */ + public function color($color) + { + $this->color = $color; + } + + /** + * Get color of text + * + * @return mixed + */ + public function getColor() + { + return $this->color; + } + + /** + * Set rotation angle of text + * + * @param integer $angle + * @return void + */ + public function angle($angle) + { + $this->angle = $angle; + } + + /** + * Get rotation angle of text + * + * @return integer + */ + public function getAngle() + { + return $this->angle; + } + + /** + * Set horizontal text alignment + * + * @param string $align + * @return void + */ + public function align($align) + { + $this->align = $align; + } + + /** + * Get horizontal text alignment + * + * @return string + */ + public function getAlign() + { + return $this->align; + } + + /** + * Set vertical text alignment + * + * @param string $valign + * @return void + */ + public function valign($valign) + { + $this->valign = $valign; + } + + /** + * Get vertical text alignment + * + * @return string + */ + public function getValign() + { + return $this->valign; + } + + /** + * Set path to font file + * + * @param string $align + * @return void + */ + public function file($file) + { + $this->file = $file; + } + + /** + * Get path to font file + * + * @return string + */ + public function getFile() + { + return $this->file; + } + + /** + * Checks if current font has access to an applicable font file + * + * @return boolean [description] + */ + private function hasApplicableFontFile() + { + if (is_string($this->file)) { + return file_exists($this->file); + } + + return false; + } + + /** + * Filter function to access internal integer font values + * + * @return integer + */ + private function getInternalFont() + { + $internalfont = is_null($this->file) ? 1 : $this->file; + $internalfont = is_numeric($internalfont) ? $internalfont : false; + + if ( ! in_array($internalfont, array(1, 2, 3, 4, 5))) { + throw new Exception\FontNotFoundException(sprintf('Internal font %s not available.', $internalfont)); + } + + return intval($internalfont); + } + + /** + * Get width of an internal font character + * + * @return integer + */ + private function getInternalFontWidth() + { + return $this->getInternalFont() + 4; + } + + /** + * Get height of an internal font character + * + * @return integer + */ + private function getInternalFontHeight() + { + switch ($this->getInternalFont()) { + case 1: + return 8; + + case 2: + return 14; + + case 3: + return 14; + + case 4: + return 16; + + case 5: + return 16; + } + } + + /** + * Calculates bounding box of current font setting + * + * @return Array + */ + public function getBoxSize() + { + $box = array(); + + if ($this->hasApplicableFontFile()) { + + // get bounding box with angle 0 + $box = imagettfbbox($this->getPointSize(), 0, $this->file, $this->text); + + // rotate points manually + if ($this->angle != 0) { + + $angle = pi() * 2 - $this->angle * pi() * 2 / 360; + + for ($i=0; $i<4; $i++) { + $x = $box[$i * 2]; + $y = $box[$i * 2 + 1]; + $box[$i * 2] = cos($angle) * $x - sin($angle) * $y; + $box[$i * 2 + 1] = sin($angle) * $x + cos($angle) * $y; + } + } + + $box['width'] = intval(abs($box[4] - $box[0])); + $box['height'] = intval(abs($box[5] - $box[1])); + + } else { + + // get current internal font size + $width = $this->getInternalFontWidth(); + $height = $this->getInternalFontHeight(); + + if (strlen($this->text) == 0) { + // no text -> no boxsize + $box['width'] = 0; + $box['height'] = 0; + } else { + // calculate boxsize + $box['width'] = strlen($this->text) * $width; + $box['height'] = $height; + } + } + + return $box; + } + + /** + * Draws font to given image at given position + * @param Image $image + * @param integer $posx + * @param integer $posy + * @return void + */ + public function applyToImage(Image $image, $posx = 0, $posy = 0) + { + // parse text color + $color = $image->parseColor($this->color); + + if ($this->hasApplicableFontFile()) { + + if ($this->angle != 0 || is_string($this->align) || is_string($this->valign)) { + + $box = $this->getBoxSize(); + + $align = is_null($this->align) ? 'left' : strtolower($this->align); + $valign = is_null($this->valign) ? 'bottom' : strtolower($this->valign); + + // correction on position depending on v/h alignment + switch ($align.'-'.$valign) { + + case 'center-top': + $posx = $posx - round(($box[6]+$box[4])/2); + $posy = $posy - round(($box[7]+$box[5])/2); + break; + + case 'right-top': + $posx = $posx - $box[4]; + $posy = $posy - $box[5]; + break; + + case 'left-top': + $posx = $posx - $box[6]; + $posy = $posy - $box[7]; + break; + + case 'center-center': + case 'center-middle': + $posx = $posx - round(($box[0]+$box[4])/2); + $posy = $posy - round(($box[1]+$box[5])/2); + break; + + case 'right-center': + case 'right-middle': + $posx = $posx - round(($box[2]+$box[4])/2); + $posy = $posy - round(($box[3]+$box[5])/2); + break; + + case 'left-center': + case 'left-middle': + $posx = $posx - round(($box[0]+$box[6])/2); + $posy = $posy - round(($box[1]+$box[7])/2); + break; + + case 'center-bottom': + $posx = $posx - round(($box[0]+$box[2])/2); + $posy = $posy - round(($box[1]+$box[3])/2); + break; + + case 'right-bottom': + $posx = $posx - $box[2]; + $posy = $posy - $box[3]; + break; + + case 'left-bottom': + $posx = $posx - $box[0]; + $posy = $posy - $box[1]; + break; + } + } + + // $image->rectangle(array(0,0,0,0.5), $posx+$box[6], $posy+$box[7], $posx+$box[2], $posy+$box[3]); + + // enable alphablending for imagettftext + imagealphablending($image->resource, true); + + // draw ttf text + imagettftext($image->resource, $this->getPointSize(), $this->angle, $posx, $posy, $color, $this->file, $this->text); + + } else { + + // get box size + $box = $this->getBoxSize(); + $width = $box['width']; + $height = $box['height']; + + // internal font specific position corrections + if ($this->getInternalFont() == 1) { + $top_correction = 1; + $bottom_correction = 2; + } elseif ($this->getInternalFont() == 3) { + $top_correction = 2; + $bottom_correction = 4; + } else { + $top_correction = 3; + $bottom_correction = 4; + } + + // x-position corrections for horizontal alignment + switch (strtolower($this->align)) { + case 'center': + $posx = ceil($posx - ($width / 2)); + break; + + case 'right': + $posx = ceil($posx - $width) + 1; + break; + } + + // y-position corrections for vertical alignment + switch (strtolower($this->valign)) { + case 'center': + case 'middle': + $posy = ceil($posy - ($height / 2)); + break; + + case 'top': + $posy = ceil($posy - $top_correction); + break; + + default: + case 'bottom': + $posy = round($posy - $height + $bottom_correction); + break; + } + + // draw text + imagestring($image->resource, $this->getInternalFont(), $posx, $posy, $this->text, $color); + } + } +} diff --git a/src/Intervention/Image/Image.php b/src/Intervention/Image/Image.php index 452810e0..56209b1a 100644 --- a/src/Intervention/Image/Image.php +++ b/src/Intervention/Image/Image.php @@ -1183,7 +1183,50 @@ class Image } /** - * Write text in current image + * Compatibility method to decide old or new style of text writing + * + * @param string $text + * @param integer $posx + * @param integer $posy + * @param integer $angle + * @param integer $size + * @param string $color + * @param string $fontfile + * @return Image + */ + public function text($text, $posx = 0, $posy = 0, $size_or_callback = null, $color = '000000', $angle = 0, $fontfile = null) + { + if (is_numeric($size_or_callback)) { + return $this->legacyText($text, $posx, $posy, $size_or_callback, $color, $angle, $fontfile); + } else { + return $this->textCallback($text, $posx, $posy, $size_or_callback); + } + } + + /** + * Write text in current image, define details via callback + * + * @param string $text + * @param integer $posx + * @param integer $posy + * @param Closure $callback + * @return Image + */ + public function textCallback($text, $posx = 0, $posy = 0, Closure $callback = null) + { + $font = new \Intervention\Image\Font($text); + + if ($callback instanceof Closure) { + $callback($font); + } + + $font->applyToImage($this, $posx, $posy); + + return $this; + } + + /** + * Legacy method to keep support of old style of text writing * * @param string $text * @param integer $pos_x @@ -1194,7 +1237,7 @@ class Image * @param string $fontfile * @return Image */ - public function text($text, $pos_x = 0, $pos_y = 0, $size = 16, $color = '000000', $angle = 0, $fontfile = null) + public function legacyText($text, $pos_x = 0, $pos_y = 0, $size = 16, $color = '000000', $angle = 0, $fontfile = null) { if (is_null($fontfile)) { @@ -1922,6 +1965,24 @@ class Image is_resource($this->original) ? imagedestroy($this->original) : null; } + /** + * Calculates checksum of current image + * + * @return String + */ + public function checksum() + { + $colors = array(); + + for ($x=0; $x <= ($this->width-1); $x++) { + for ($y=0; $y <= ($this->height-1); $y++) { + $colors[] = $this->pickColor($x, $y, 'int'); + } + } + + return md5(serialize($colors)); + } + /** * Returns image stream * diff --git a/tests/FontTest.php b/tests/FontTest.php new file mode 100644 index 00000000..0f092919 --- /dev/null +++ b/tests/FontTest.php @@ -0,0 +1,345 @@ +assertInstanceOf('Intervention\Image\Font', $font); + } + + public function testConstructorWithParameters() + { + $font = new Font('The quick brown fox jumps over the lazy dog.'); + $this->assertInstanceOf('Intervention\Image\Font', $font); + $this->assertEquals($font->getText(), 'The quick brown fox jumps over the lazy dog.'); + } + + public function testText() + { + $font = new Font; + $font->text('The quick brown fox jumps over the lazy dog.'); + $this->assertEquals($font->getText(), 'The quick brown fox jumps over the lazy dog.'); + } + + public function testSize() + { + $font = new Font; + $font->size(24); + $this->assertInternalType('int', $font->getSize()); + $this->assertEquals($font->getSize(), 24); + } + + public function testPointSize() + { + $font = new Font; + $font->size(24); + $this->assertInternalType('int', $font->getPointSize()); + $this->assertEquals($font->getPointSize(), 18); + } + + public function testColor() + { + $font = new Font; + $font->color('b53717'); + $this->assertInternalType('string', $font->getColor()); + $this->assertEquals($font->getColor(), 'b53717'); + } + + public function testAngle() + { + $font = new Font; + $font->angle(45); + $this->assertInternalType('int', $font->getAngle()); + $this->assertEquals($font->getAngle(), 45); + } + + public function testAlign() + { + $font = new Font; + $font->align('center'); + $this->assertInternalType('string', $font->getAlign()); + $this->assertEquals($font->getAlign(), 'center'); + } + + public function testValign() + { + $font = new Font; + $font->valign('bottom'); + $this->assertInternalType('string', $font->getValign()); + $this->assertEquals($font->getValign(), 'bottom'); + } + + public function testFile() + { + $font = new Font; + $font->file('foo.ttf'); + $this->assertInternalType('string', $font->getFile()); + $this->assertEquals($font->getFile(), 'foo.ttf'); + } + + public function testGetBoxsizeInternal() + { + $font = new Font; + $box = $font->getboxsize(); + $this->assertInternalType('int', $box['width']); + $this->assertInternalType('int', $box['height']); + $this->assertEquals(0, $box['width']); + $this->assertEquals(0, $box['height']); + + $font = new Font('000'); + $box = $font->getboxsize(); + $this->assertInternalType('int', $box['width']); + $this->assertInternalType('int', $box['height']); + $this->assertEquals(15, $box['width']); + $this->assertEquals(8, $box['height']); + + $font = new Font('000'); + $font->file(1); + $box = $font->getboxsize(); + $this->assertInternalType('int', $box['width']); + $this->assertInternalType('int', $box['height']); + $this->assertEquals(15, $box['width']); + $this->assertEquals(8, $box['height']); + + $font = new Font('000'); + $font->file(2); + $box = $font->getboxsize(); + $this->assertInternalType('int', $box['width']); + $this->assertInternalType('int', $box['height']); + $this->assertEquals(18, $box['width']); + $this->assertEquals(14, $box['height']); + + $font = new Font('000'); + $font->file(3); + $box = $font->getboxsize(); + $this->assertInternalType('int', $box['width']); + $this->assertInternalType('int', $box['height']); + $this->assertEquals(21, $box['width']); + $this->assertEquals(14, $box['height']); + + $font = new Font('000'); + $font->file(4); + $box = $font->getboxsize(); + $this->assertInternalType('int', $box['width']); + $this->assertInternalType('int', $box['height']); + $this->assertEquals(24, $box['width']); + $this->assertEquals(16, $box['height']); + + $font = new Font('000'); + $font->file(5); + $box = $font->getboxsize(); + $this->assertInternalType('int', $box['width']); + $this->assertInternalType('int', $box['height']); + $this->assertEquals(27, $box['width']); + $this->assertEquals(16, $box['height']); + } + + /* + public function testGetBoxsizeFontfile() + { + $font = new Font; + $font->file('public/Vera.ttf'); + $box = $font->getboxsize(); + $this->assertInternalType('int', $box['width']); + $this->assertInternalType('int', $box['height']); + $this->assertEquals(0, $box['width']); + $this->assertEquals(0, $box['height']); + + $font = new Font('000'); + $font->file('public/Vera.ttf'); + $box = $font->getboxsize(); + $this->assertInternalType('int', $box['width']); + $this->assertInternalType('int', $box['height']); + $this->assertEquals(22, $box['width']); + $this->assertEquals(9, $box['height']); + + $font = new Font('000'); + $font->file('public/Vera.ttf'); + $font->size(16); + $box = $font->getboxsize(); + $this->assertInternalType('int', $box['width']); + $this->assertInternalType('int', $box['height']); + $this->assertEquals(28, $box['width']); + $this->assertEquals(12, $box['height']); + + $font = new Font('000'); + $font->file('public/Vera.ttf'); + $font->size(24); + $box = $font->getboxsize(); + $this->assertInternalType('int', $box['width']); + $this->assertInternalType('int', $box['height']); + $this->assertEquals(42, $box['width']); + $this->assertEquals(18, $box['height']); + } + + + public function testGetBoxsizeFontfileWithAngle() + { + $font = new Font('000'); + $font->file('public/Vera.ttf'); + $font->angle(45); + $box = $font->getboxsize(); + $this->assertInternalType('int', $box['width']); + $this->assertInternalType('int', $box['height']); + $this->assertEquals(22, $box['width']); + $this->assertEquals(9, $box['height']); + + $font = new Font('000'); + $font->file('public/Vera.ttf'); + $font->size(16); + $font->angle(45); + $box = $font->getboxsize(); + $this->assertInternalType('int', $box['width']); + $this->assertInternalType('int', $box['height']); + $this->assertEquals(28, $box['width']); + $this->assertEquals(12, $box['height']); + + $font = new Font('000'); + $font->file('public/Vera.ttf'); + $font->size(24); + $font->angle(45); + $box = $font->getboxsize(); + $this->assertInternalType('int', $box['width']); + $this->assertInternalType('int', $box['height']); + $this->assertEquals(42, $box['width']); + $this->assertEquals(18, $box['height']); + } + */ + + public function testAlignmentsInternalFonts() + { + $haligns = array('left', 'center', 'right'); + $valigns = array('top', 'middle', 'bottom'); + $fontfiles = array(1, 2, 3, 4, 5); + $position = array(80, 40); + + $checksums = array( + '1' => array( + 'top' => array( + 'left' => 'fa8bfa9a8cce6071515d9aac18007a50', + 'center' => '86c9fa152b5b3e1a637cb17b70a6edfc', + 'right' => 'a386f9155ef5da46872c73958b668799', + ), + 'middle' => array( + 'left' => '45af47bbda0ba771aa4963f447d3bbb9', + 'center' => '700182599f461d37efb8297170ea7eda', + 'right' => '288bab5bf92dead0ed909167d072b8bb', + ), + 'bottom' => array( + 'left' => 'a85e0d2329957b27b3cf4c3f21721c45', + 'center' => '507a590cdae9b6dce1a6fc9b30a744cb', + 'right' => '530ac7978230241c555dd8c5374f029e', + ), + ), + '2' => array( + 'top' => array( + 'left' => '24e4dc99e7e2b9b7d553b0fce1abf3a3', + 'center' => '1099aae99acad2dac796d33da99cb9d3', + 'right' => '06a888438b2a6bc5e673f8c6d9b70e42', + ), + 'middle' => array( + 'left' => 'ae2629402f215aef11226413b71b8b7a', + 'center' => '8ce82b42da6ad6b5ceb7da7bf637022d', + 'right' => '870034642cfd26ebd165ca0de5a8c12c', + ), + 'bottom' => array( + 'left' => 'd6b53fdd3e68f64287e988937917a6e3', + 'center' => '3a4c8fdaae0f056c3b24c2d13d09a865', + 'right' => 'd4db9c844cf73a78b0ddadaa7a230c04', + ), + ), + '3' => array( + 'top' => array( + 'left' => 'd6dd07b730f8793ba08b5f2052965254', + 'center' => 'b8ae1ca956b74b066572dd76c1fbc2cb', + 'right' => '1e3327bf7e540487aa1514e490339ff3', + ), + 'middle' => array( + 'left' => '2b69adba28256ac1b10d67bde6f2ac71', + 'center' => '55cdc747bd51513640be777d15561f58', + 'right' => '1918bcf0810b38454d84591b356a5f1e', + ), + 'bottom' => array( + 'left' => 'aac8e8a56e9f464b5223c568fe660ff9', + 'center' => 'e9c381bfac9690e1b7c330b9ed9bd6fa', + 'right' => '3966301d2c445fe5f73c10bda7ba7993', + ), + ), + '4' => array( + 'top' => array( + 'left' => '9b61dcbbf8c1d61db7301f38677cb094', + 'center' => 'b3c6f738493d38ba6e658100fa5592f7', + 'right' => '65b7525ee23c7e4d3db3e82b24b3f175', + ), + 'middle' => array( + 'left' => '060933279d8a34d0234c1d0e25c41357', + 'center' => 'f06c06b4604a72f7b8b9068ffa306990', + 'right' => '3cc4f152c671021decca21656ac078a2', + ), + 'bottom' => array( + 'left' => '83201c48862f4ccf218b7ae018cfc61f', + 'center' => 'e07fd632d487f1fe507c4adf7c4a8f71', + 'right' => 'da2cf30237fcd2724ba2b7248026d73b', + ), + ), + '5' => array( + 'top' => array( + 'left' => '02cabb064130730206bfc05d86842bcd', + 'center' => '50c46cf1ccf9776cc118ad1102a3f259', + 'right' => 'e1e7bc8a72c0b64b20cc3e96e4cb7573', + ), + 'middle' => array( + 'left' => '45f263ba8a4ae63f4275a8b76a0f526b', + 'center' => 'a76f4d65901b30ed5eb58a9975560b80', + 'right' => '7d04bceecc1c576e84b6184a89b2b2c7', + ), + 'bottom' => array( + 'left' => '285741dbdb636724dc56b2ff8a0c5814', + 'center' => 'ed274bfc9d9e3e7644baedc043312f7b', + 'right' => 'fa00447bb085ec03d6a865bbc39faf36', + ), + ), + ); + + foreach ($haligns as $halign) { + foreach ($valigns as $valign) { + foreach ($fontfiles as $file) { + $canvas = new Image(null, 160, 80, 'ffffff'); + $font = new Font('00000'); + $font->file($file); + $font->align($halign); + $font->valign($valign); + $font->applyToImage($canvas, $position[0], $position[1]); + $checksum = $canvas->checksum(); + $this->assertEquals($checksum, $checksums[$file][$valign][$halign]); + } + } + } + } + + /** + * @expectedException Intervention\Image\Exception\FontNotFoundException + */ + public function testInternalFontNotAvailable() + { + $image = new Image(null, 25, 25); + $font = new Font; + $font->file(10); + $font->applyToImage($image); + } + + /** + * @expectedException Intervention\Image\Exception\FontNotFoundException + */ + public function testFontfileNotAvailable() + { + $image = new Image(null, 25, 25); + $font = new Font; + $font->file('foo/bar.ttf'); + $font->applyToImage($image); + } +} diff --git a/tests/ImageTest.php b/tests/ImageTest.php index 7e1d1570..61330ef5 100644 --- a/tests/ImageTest.php +++ b/tests/ImageTest.php @@ -1292,7 +1292,7 @@ class ImageTest extends PHPUnit_Framework_Testcase $this->assertEquals('#ffffff', $img->pickColor($coords[1][0], $coords[1][1], 'hex')); } - public function testTextImage() + public function testLegacyTextImage() { $img = $this->getTestImage(); $img = $img->text('Fox', 10, 10, 16, '000000', 0, null); @@ -1305,6 +1305,33 @@ class ImageTest extends PHPUnit_Framework_Testcase $this->assertInstanceOf('Intervention\Image\Image', $img); } + public function testTextImage() + { + $img = new Image(null, 160, 80, 'ffffff'); + $img = $img->text('00000', 80, 40); + $this->assertInstanceOf('Intervention\Image\Image', $img); + $this->assertEquals('a85e0d2329957b27b3cf4c3f21721c45', $img->checksum()); + + $img = new Image(null, 160, 80, 'ffffff'); + $img = $img->text('00000', 80, 40, function($font) { + $font->align('center'); + $font->valign('top'); + $font->color('000000'); + }); + $this->assertInstanceOf('Intervention\Image\Image', $img); + $this->assertEquals('86c9fa152b5b3e1a637cb17b70a6edfc', $img->checksum()); + + $img = new Image(null, 160, 80, 'ffffff'); + $img = $img->text('00000', 80, 40, function($font) { + $font->align('right'); + $font->valign('middle'); + $font->file(2); + $font->color('000000'); + }); + $this->assertInstanceOf('Intervention\Image\Image', $img); + $this->assertEquals('870034642cfd26ebd165ca0de5a8c12c', $img->checksum()); + } + public function testRectangleImage() { $img = $this->getTestImage(); @@ -1834,4 +1861,11 @@ class ImageTest extends PHPUnit_Framework_Testcase $img->destroy(); $this->assertEquals(get_resource_type($img->resource), 'Unknown'); } + + public function testChecksum() + { + $img = new Image('public/circle.png'); + $checksum = $img->checksum(); + $this->assertEquals($checksum, '149432c4e99e8bf8c295afb85be64e78'); + } }