├── 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 |
--------------------------------------------------------------------------------