├── selftest ├── hd.png ├── cloak.png ├── default.png ├── skin64.png ├── hd_cloak.png ├── tiny_arms.png └── skinTest.php ├── README.md └── SkinViewer2D.class.php /selftest/hd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NC22/Minecraft-HD-skin-viewer-2D/HEAD/selftest/hd.png -------------------------------------------------------------------------------- /selftest/cloak.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NC22/Minecraft-HD-skin-viewer-2D/HEAD/selftest/cloak.png -------------------------------------------------------------------------------- /selftest/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NC22/Minecraft-HD-skin-viewer-2D/HEAD/selftest/default.png -------------------------------------------------------------------------------- /selftest/skin64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NC22/Minecraft-HD-skin-viewer-2D/HEAD/selftest/skin64.png -------------------------------------------------------------------------------- /selftest/hd_cloak.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NC22/Minecraft-HD-skin-viewer-2D/HEAD/selftest/hd_cloak.png -------------------------------------------------------------------------------- /selftest/tiny_arms.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NC22/Minecraft-HD-skin-viewer-2D/HEAD/selftest/tiny_arms.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Minecraft skin viewer-2D 2 | =========================== 3 | 4 | PHP class - viewer for minecraft skins; generate images of front \ rare \ head view of skin; Support cloaks and HD images 5 | 6 | Require GD module 7 | 8 | - **Author** : NC22 9 | - **Version** : 1.21b 10 | 11 | ## Information 12 | 13 | Look 'selftest' dirrectory for examples. 14 | 15 | Support 64x64 skin format since version 1.2b 16 | 17 | Support "slimmer arms" feature since version 1.21b 18 | 19 | ## License 20 | 21 | [GNU General Public License v3](http://www.gnu.org/licenses/gpl.html) 22 | -------------------------------------------------------------------------------- /selftest/skinTest.php: -------------------------------------------------------------------------------- 1 | 6 | * @copyright 2013-2016 Rubchuk Vladimir 7 | * @version 1.21b 8 | * @license GPLv3 9 | * 10 | * Special thanks to Official Minecraft Wiki 11 | * Render based on templates from 12 | * http://minecraft.gamepedia.com/Skin 13 | * 14 | * 15 | * Script for testing functional of class "SkinGenerator2D" 16 | * 17 | */ 18 | 19 | if (isset($_GET['show'])) { 20 | 21 | require '../SkinViewer2D.class.php'; 22 | header("Content-type: image/png"); 23 | 24 | $show = $_GET['show']; 25 | 26 | $baseDir = dirname(__FILE__) . '/'; 27 | $skin = empty($_GET['file_name']) ? 'default' : $_GET['file_name']; 28 | $skin = $baseDir . $skin . '.png'; 29 | 30 | if (!skinViewer2D::isValidSkin($skin)) { 31 | $skin = $baseDir . 'default.png'; 32 | } 33 | 34 | if ($show !== 'head') { 35 | $cloak = isset($_GET['cloak']) ? (($_GET['cloak'] === 'hd') ? './hd_cloak.png' : './cloak.png') : false; 36 | $side = isset($_GET['side']) ? $_GET['side'] : false; 37 | 38 | $img = skinViewer2D::createPreview($skin, $cloak, $side); 39 | } else { 40 | $img = skinViewer2D::createHead($skin, 64); 41 | } 42 | 43 | imagepng($img); 44 | 45 | } else { 46 | ?> 47 | 48 | 49 | 50 | 51 |

52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 |

63 | 64 | 65 |

66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 |

74 | 75 | 76 |

77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 |

85 | 86 | 87 |

88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 |

96 | 97 | 98 | -------------------------------------------------------------------------------- /SkinViewer2D.class.php: -------------------------------------------------------------------------------- 1 | 6 | * @copyright 2013-2016 Rubchuk Vladimir 7 | * @version 1.21b 8 | * @license GPLv3 9 | * 10 | * Special thanks to Official Minecraft Wiki 11 | * Based on templates from 12 | * http://minecraft.gamepedia.com/Skin 13 | */ 14 | 15 | class SkinViewer2D 16 | { 17 | private static $slimDetectPixel = array(42, 51); // x,y 18 | 19 | /* Допустимые пропорции образа */ 20 | 21 | private static $skinProps = array( 22 | 0 => array('base' => 64, 'ratio' => 2), 23 | 1 => array('base' => 64, 'ratio' => 1), 24 | ); 25 | 26 | /* 27 | * Массив допустимых пропорций плаща (для плаща в MC нет четкой привязки к размеру) 28 | * Некоторые плащи используют соотношение 22x17, тогда как обычно используется 29 | * соотношение 64x32 с незаполненным пространством 30 | */ 31 | 32 | private static $cloakProps = array( 33 | 0 => array('base' => 64, 'ratio' => 2), 34 | 1 => array('base' => 22, 'ratio' => 1.29), 35 | ); 36 | 37 | /** 38 | * Создает изображение головы; вид спереди 39 | * @param string $way_skin полный путь до файла изображения скина 40 | * @param int $size размер возвращаемого изображения в пикселях 41 | * @return resource Возвращает идентификатор GD image при успешном результате и false при ошибке 42 | */ 43 | 44 | public static function createHead($way_skin, $size = 151) 45 | { 46 | if (!$info = self::isValidSkin($way_skin)) 47 | return false; 48 | 49 | $img = @imagecreatefrompng($way_skin); 50 | if (!$img) 51 | return false; 52 | 53 | $p = array('face' => array(8, 8), 'hat' => array(40, 8)); 54 | 55 | 56 | $av = imagecreatetruecolor($size, $size); 57 | $mp = $info['scale']; 58 | 59 | imagecopyresized($av, $img, 0, 0, $p['face'][0] * $mp, $p['face'][1] * $mp, $size, $size, 8 * $mp, 8 * $mp); 60 | imagecopyresized($av, $img, 0, 0, $p['hat'][0] * $mp, $p['hat'][1] * $mp, $size, $size, 8 * $mp, 8 * $mp); 61 | imagedestroy($img); 62 | 63 | return $av; 64 | } 65 | 66 | /** 67 | * Создать видовое изображение из скина; фронтальный \ задний вид 68 | * @param string $way_skin полный путь до файла изображения скина 69 | * @param string $way_cloak полный путь до файла изображения плаща ( при необходимости ) 70 | * @param string $side вид спереди - front \ вид сзади - back \ по умолчанию оба вида на одном изображении последовательно 71 | * @param int $size высота возвращаемого изображения в пикселях (ширина пропорцианальна задаваемой высоте и завист так же от параметра $side) 72 | * @return resource Возвращает идентификатор GD image при успешном результате и false при ошибке 73 | */ 74 | 75 | public static function createPreview($way_skin, $way_cloak = false, $side = false, $size = 224) 76 | { 77 | if (!$info = self::isValidSkin($way_skin)) 78 | return false; 79 | 80 | $skin = @imagecreatefrompng($way_skin); 81 | if (!$skin) 82 | return false; 83 | 84 | $mp = $info['scale']; 85 | $size_x = (($side) ? 16 : 32); 86 | $preview = imagecreatetruecolor($size_x * $mp, 32 * $mp); 87 | 88 | $transparent = imagecolorallocatealpha($preview, 255, 255, 255, 127); 89 | imagefill($preview, 0, 0, $transparent); 90 | 91 | $armWidth = 4; // for slim \ fat arms on version 1.8 or higher 92 | $slim = false; 93 | 94 | if ($info['ratio'] == 1) { 95 | 96 | // is slim verion 97 | 98 | $color = imagecolorat($skin, self::$slimDetectPixel[0], self::$slimDetectPixel[1]); 99 | $colors = imagecolorsforindex($skin, $color); // returns rgba array 100 | 101 | if ((int) $colors['alpha'] == 127) { 102 | $slim = true; 103 | $armWidth = 3; 104 | } 105 | } 106 | 107 | if (!$side or $side === 'front') { 108 | 109 | // head 110 | imagecopy($preview, $skin, 4 * $mp, 0 * $mp, 8 * $mp, 8 * $mp, 8 * $mp, 8 * $mp); 111 | imagecopy($preview, $skin, 4 * $mp, 0 * $mp, 40 * $mp, 8 * $mp, 8 * $mp, 8 * $mp); 112 | 113 | // front arms 114 | 115 | imagecopy($preview, $skin, (4 - $armWidth) * $mp, 8 * $mp, 44 * $mp, 20 * $mp, $armWidth * $mp, 12 * $mp); 116 | 117 | // right side 118 | if ($info['ratio'] == 2) { 119 | self::imageflip($preview, $skin, 12 * $mp, 8 * $mp, 44 * $mp, 20 * $mp, 4 * $mp, 12 * $mp); 120 | } else { 121 | // body (8) + arm(4) 122 | imagecopy($preview, $skin, 12 * $mp, 8 * $mp, 36 * $mp, 52 * $mp, $armWidth * $mp, 12 * $mp); 123 | } 124 | 125 | // body 126 | imagecopy($preview, $skin, 4 * $mp, 8 * $mp, 20 * $mp, 20 * $mp, 8 * $mp, 12 * $mp); 127 | 128 | // front legs 129 | imagecopy($preview, $skin, 4 * $mp, 20 * $mp, 4 * $mp, 20 * $mp, 4 * $mp, 12 * $mp); 130 | 131 | if ($info['ratio'] == 2) { 132 | self::imageflip($preview, $skin, 8 * $mp, 20 * $mp, 4 * $mp, 20 * $mp, 4 * $mp, 12 * $mp); 133 | } else { 134 | imagecopy($preview, $skin, 8 * $mp, 20 * $mp, 20 * $mp, 52 * $mp, 4 * $mp, 12 * $mp); 135 | } 136 | 137 | if ($info['ratio'] == 1) { 138 | // front arms layer 2 right 139 | imagecopy($preview, $skin, (4 - $armWidth) * $mp, 8 * $mp, 44 * $mp, 36 * $mp, $armWidth * $mp, 12 * $mp); 140 | // left 141 | imagecopy($preview, $skin, 12 * $mp, 8 * $mp, 52 * $mp, 52 * $mp, $armWidth * $mp, 12 * $mp); 142 | 143 | // jacket 144 | imagecopy($preview, $skin, 4 * $mp, 8 * $mp, 20 * $mp, 36 * $mp, 8 * $mp, 12 * $mp); 145 | 146 | // front legs right leg layer 2 147 | imagecopy($preview, $skin, 4 * $mp, 20 * $mp, 4 * $mp, 36 * $mp, 4 * $mp, 12 * $mp); 148 | // front legs left leg layer 2 149 | imagecopy($preview, $skin, 8 * $mp, 20 * $mp, 4 * $mp, 52 * $mp, 4 * $mp, 12 * $mp); 150 | } 151 | 152 | } 153 | if (!$side or $side === 'back') { 154 | 155 | $mp_x_h = ($side) ? 0 : imagesx($preview) / 2; // base padding left on output canvas, if render both sides on the same image 156 | $backArmPos = ($armWidth * 2); 157 | 158 | // front side of arm have width 3, but back still able have width 4, so skip pixels at begining 159 | 160 | if ($armWidth < 4) { 161 | $backArmPos += 4 - $armWidth; 162 | } 163 | 164 | // head 165 | imagecopy($preview, $skin, $mp_x_h + 4 * $mp, 0 * $mp, 24 * $mp, 8 * $mp, 8 * $mp, 8 * $mp); 166 | imagecopy($preview, $skin, $mp_x_h + 4 * $mp, 0 * $mp, 56 * $mp, 8 * $mp, 8 * $mp, 8 * $mp); 167 | 168 | // body back 169 | imagecopy($preview, $skin, $mp_x_h + 4 * $mp, 8 * $mp, 32 * $mp, 20 * $mp, 8 * $mp, 12 * $mp); 170 | 171 | // back arm, calc from start right arm zone base on arm width 172 | 173 | imagecopy($preview, $skin, $mp_x_h + 12 * $mp, 8 * $mp, (44 + $backArmPos) * $mp, 20 * $mp, $armWidth * $mp, 12 * $mp); 174 | 175 | // flip left arm for old skins 176 | 177 | if ($info['ratio'] == 2) { 178 | self::imageflip($preview, $skin, $mp_x_h + 0 * $mp, 8 * $mp, 52 * $mp, 20 * $mp, 4 * $mp, 12 * $mp); 179 | } else { 180 | imagecopy($preview, $skin, $mp_x_h + (4 - $armWidth) * $mp, 8 * $mp, (36 + $backArmPos) * $mp, 52 * $mp, $armWidth * $mp, 12 * $mp); 181 | } 182 | 183 | // back leg 184 | 185 | // left 186 | if ($info['ratio'] == 2) { 187 | self::imageflip($preview, $skin, $mp_x_h + 4 * $mp, 20 * $mp, 12 * $mp, 20 * $mp, 4 * $mp, 12 * $mp); 188 | } else { 189 | imagecopy($preview, $skin, $mp_x_h + 4 * $mp, 20 * $mp, 28 * $mp, 52 * $mp, 4 * $mp, 12 * $mp); 190 | } 191 | 192 | // right 193 | imagecopy($preview, $skin, $mp_x_h + 8 * $mp, 20 * $mp, 12 * $mp, 20 * $mp, 4 * $mp, 12 * $mp); 194 | 195 | // addition attributes for new skins (v 1.8 >) 196 | if ($info['ratio'] == 1) { 197 | // jaket 198 | imagecopy($preview, $skin, $mp_x_h + 4 * $mp, 8 * $mp, 32 * $mp, 36 * $mp, 8 * $mp, 12 * $mp); 199 | 200 | // back arm decals right arm 201 | imagecopy($preview, $skin, $mp_x_h + 12 * $mp, 8 * $mp, (44 + $backArmPos) * $mp, 36 * $mp, $armWidth * $mp, 12 * $mp); 202 | // back arm decals left arm 203 | imagecopy($preview, $skin, $mp_x_h + (4 - $armWidth) * $mp, 8 * $mp, (52 + $backArmPos) * $mp, 52 * $mp, $armWidth * $mp, 12 * $mp); 204 | 205 | // back leg decals 2 right leg 206 | imagecopy($preview, $skin, $mp_x_h + 8 * $mp, 20 * $mp, 12 * $mp, 36 * $mp, 4 * $mp, 12 * $mp); 207 | // back leg decals 2 left leg 208 | imagecopy($preview, $skin, $mp_x_h + 4 * $mp, 20 * $mp, 12 * $mp, 52 * $mp, 4 * $mp, 12 * $mp); 209 | } 210 | } 211 | 212 | if ($way_cloak and !$info = self::isValidCloak($way_cloak)) { 213 | $way_cloak = null; 214 | } else { 215 | $mp_cloak = $info['scale']; 216 | } 217 | 218 | $cloak = @imagecreatefrompng($way_cloak); 219 | if (!$cloak) 220 | $way_cloak = null; 221 | 222 | if ($way_cloak) { 223 | 224 | if ($mp_cloak > $mp) { // cloak bigger 225 | $mp_x_h = ($side) ? 0 : ($size_x * $mp_cloak) / 2; 226 | $mp_result = $mp_cloak; 227 | } else { 228 | $mp_x_h = ($side) ? 0 : ($size_x * $mp) / 2; 229 | $mp_result = $mp; 230 | } 231 | 232 | $preview_cloak = imagecreatetruecolor($size_x * $mp_result, 32 * $mp_result); 233 | $transparent = imagecolorallocatealpha($preview_cloak, 255, 255, 255, 127); 234 | imagefill($preview_cloak, 0, 0, $transparent); 235 | 236 | // ex. copy front side of cloak to new image 237 | 238 | if (!$side or $side === 'front') 239 | imagecopyresized( 240 | $preview_cloak, // result image 241 | $cloak, // source image 242 | round(3 * $mp_result), // start x point of result 243 | round(8 * $mp_result), // start y point of result 244 | round(12 * $mp_cloak), // start x point of source img 245 | round(1 * $mp_cloak), // start y point of source img 246 | round(10 * $mp_result), // result <- width -> 247 | round(16 * $mp_result), // result /|\ height \|/ 248 | round(10 * $mp_cloak), // width of cloak img (from start x \ y) 249 | round(16 * $mp_cloak) // height of cloak img (from start x \ y) 250 | ); 251 | 252 | imagecopyresized($preview_cloak, $preview, 0, 0, 0, 0, imagesx($preview_cloak), imagesy($preview_cloak), imagesx($preview), imagesy($preview)); 253 | 254 | if (!$side or $side === 'back') 255 | imagecopyresized( 256 | $preview_cloak, 257 | $cloak, 258 | $mp_x_h + 3 * $mp_result, 259 | round(8 * $mp_result), 260 | round(1 * $mp_cloak), 261 | round(1 * $mp_cloak), 262 | round(10 * $mp_result), 263 | round(16 * $mp_result), 264 | round(10 * $mp_cloak), 265 | round(16 * $mp_cloak) 266 | ); 267 | 268 | $preview = $preview_cloak; 269 | } 270 | 271 | $size_x = ($side) ? $size / 2 : $size; 272 | $fullsize = imagecreatetruecolor($size_x, $size); 273 | 274 | imagesavealpha($fullsize, true); 275 | $transparent = imagecolorallocatealpha($fullsize, 255, 255, 255, 127); 276 | imagefill($fullsize, 0, 0, $transparent); 277 | 278 | imagecopyresized($fullsize, $preview, 0, 0, 0, 0, imagesx($fullsize), imagesy($fullsize), imagesx($preview), imagesy($preview)); 279 | 280 | imagedestroy($preview); 281 | imagedestroy($skin); 282 | if ($way_cloak) 283 | imagedestroy($cloak); 284 | 285 | return $fullsize; 286 | } 287 | 288 | /** 289 | * Сохранить изображение в формате png; отрисованое по правилам createPreview 290 | * @param string $way_save путь до сохраняемого файла 291 | * @param string $way_skin полный путь до файла изображения скина 292 | * @param string $way_cloak полный путь до файла изображения плаща ( при необходимости ) 293 | * @param string $side вид спереди - front \ вид сзади - back \ по умолчанию обе стороны 294 | * @param int $size высота возвращаемого изображения в пикселях (ширина пропорцианальна задаваемой высоте и завист так же от параметра $side) 295 | * @return resource Возвращает идентификатор GD image при успешном результате и false при ошибке 296 | */ 297 | 298 | public static function savePreview($way_save, $way_skin, $way_cloak = false, $side = false, $size = 224) 299 | { 300 | if (file_exists($way_save)) 301 | unlink($way_save); 302 | 303 | $new_skin = self::createPreview($way_skin, $way_cloak, $side, $size); 304 | if (!$new_skin) 305 | return false; 306 | 307 | imagepng($new_skin, $way_save); 308 | return $new_skin; 309 | } 310 | 311 | /** 312 | * Сохранить изображение в формате png; отрисованое по правилам createHead 313 | * @param int $size размер возвращаемого изображения в пикселях для одной стороны 314 | * @param string $way_save путь до сохраняемого файла 315 | * @param string $way_skin полный путь до файла изображения скина 316 | * @return resource Возвращает идентификатор GD image при успешном результате и false при ошибке 317 | */ 318 | 319 | public static function saveHead($way_save, $way_skin, $size = 151) 320 | { 321 | if (file_exists($way_save)) 322 | unlink($way_save); 323 | 324 | $new_head = self::createHead($way_skin, $size); 325 | if (!$new_head) 326 | return false; 327 | 328 | imagepng($new_head, $way_save); 329 | return $new_head; 330 | } 331 | 332 | /** 333 | * Проверить, является ли файл изображением, с соответствующими для скина пропорциями 334 | * @param string $way_skin полный путь до файла изображения скина 335 | * @return array Если файл не проходит проверку возвращает false, иначе возвращает массив пропорций изображения 336 | */ 337 | 338 | public static function isValidSkin($way_skin) 339 | { 340 | if (!file_exists($way_skin)) 341 | return false; 342 | 343 | if (!$imageSize = self::getImageSize($way_skin)) 344 | return false; 345 | 346 | 347 | for ($i = 0; $i < sizeof(self::$skinProps); $i++) { 348 | if (round(self::$skinProps[$i]['ratio'], 2) != self::getRatio($imageSize)) 349 | continue; 350 | 351 | return array( 352 | 'ratio' => self::getRatio($imageSize), 353 | 'scale' => self::getScale($imageSize, self::$skinProps[$i]['base']), 354 | ); 355 | } 356 | 357 | return false; 358 | } 359 | 360 | /** 361 | * Проверить, является ли файл изображением, с соответствующими для плащя пропорциями 362 | * @param string $way_cloak полный путь до файла изображения плаща 363 | * @return array Если файл не проходит проверку возвращает false, иначе возвращает массив пропорций изображения 364 | */ 365 | 366 | public static function isValidCloak($way_cloak) 367 | { 368 | if (!file_exists($way_cloak)) 369 | return false; 370 | if (!$imageSize = self::getImageSize($way_cloak)) 371 | return false; 372 | 373 | for ($i = 0; $i < sizeof(self::$cloakProps); $i++) { 374 | if (round(self::$cloakProps[$i]['ratio'], 2) != self::getRatio($imageSize)) 375 | continue; 376 | 377 | return array( 378 | 'ratio' => self::$cloakProps[$i]['ratio'], 379 | 'scale' => self::getScale($imageSize, self::$cloakProps[$i]['base']), 380 | ); 381 | } 382 | 383 | return false; 384 | } 385 | 386 | /* Коэфициэнт масштабирования относительно базового разрешения */ 387 | 388 | private static function getScale($inputImg, $size) 389 | { 390 | if (!is_array($inputImg) and !$inputImg = self::getImageSize($inputImg)) 391 | return false; 392 | return $inputImg[0] / $size; 393 | } 394 | 395 | /* Коэфициэнт соотношения сторон */ 396 | private static function getRatio($inputImg) 397 | { 398 | if (!is_array($inputImg) and !$inputImg = self::getImageSize($inputImg)) 399 | return false; 400 | return round($inputImg[0] / $inputImg[1], 2); 401 | } 402 | 403 | private static function getImageSize($file) 404 | { 405 | $imageSize = @getimagesize($file); 406 | 407 | if (empty($imageSize)) 408 | return false; 409 | return $imageSize; 410 | } 411 | 412 | private static function imageflip(&$result, &$img, $rx = 0, $ry = 0, $x = 0, $y = 0, $size_x = null, $size_y = null) 413 | { 414 | if ($size_x < 1) 415 | $size_x = imagesx($img); 416 | if ($size_y < 1) 417 | $size_y = imagesy($img); 418 | 419 | imagecopyresampled($result, $img, $rx, $ry, ($x + $size_x - 1), $y, $size_x, $size_y, 0 - $size_x, $size_y); 420 | } 421 | } 422 | --------------------------------------------------------------------------------