IsometricAvatar::createFromFile()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Minepic\Image;
6
7
use Minepic\Helpers\Storage\Files\IsometricsStorage;
8
use Minepic\Helpers\Storage\Files\SkinsStorage;
9
use Minepic\Image\Components\Side;
10
use Minepic\Image\Exceptions\SkinNotFountException;
11
use Minepic\Image\Sections\Avatar;
12
13
class IsometricAvatar
14
{
15
    /**
16
     * Cosine PI/6.
17
     */
18
    private const COSINE_PI_6 = \M_SQRT3 / 2;
19
20
    /**
21
     * Base size (all requests must be <=).
22
     */
23
    private const HEAD_BASE_SIZE = 252;
24
25
    /**
26
     * Margin (in pixels) in the final image.
27
     */
28
    private const HEAD_MARGIN = 4;
29
30
    /**
31
     * Maximum size for resize operation.
32
     */
33
    private const MAX_SIZE = 512;
34
35
    /**
36
     * Minimum size for resize operation.
37
     */
38
    private const MIN_SIZE = 16;
39
40
    /**
41
     * User UUID.
42
     */
43
    protected string $uuid = '';
44
45
    /**
46
     * Last time user data has been updated.
47
     */
48
    protected int $lastUpdate = 0;
49
50
    /**
51
     * Flag for checking cache.
52
     */
53
    protected bool $checkCacheStatusFlag = true;
54
55
    /**
56
     * Skin Path.
57
     */
58
    protected string $skinPath = '';
59
60
    /**
61
     * Skin Path.
62
     */
63
    protected string $isometricPath = '';
64
65
    protected \Imagick $head;
66
67
    /**
68
     * IsometricAvatar constructor.
69
     *
70
     * @param string $uuid       User UUID
71
     *
72
     * @throws SkinNotFountException
73
     */
74
    public function __construct(string $uuid, int $lastUpdate)
75
    {
76
        $this->uuid = $uuid;
77
        $this->lastUpdate = $lastUpdate;
78
79
        if (SkinsStorage::exists($uuid)) {
80
            $this->skinPath = SkinsStorage::getPath($uuid);
81
        } else {
82
            throw new SkinNotFountException();
83
        }
84
85
        if (IsometricsStorage::exists($uuid)) {
86
            $this->isometricPath = IsometricsStorage::getPath($uuid);
87
        }
88
    }
89
90
    /**
91
     * __toString().
92
     */
93
    public function __toString(): string
94
    {
95
        return (string) $this->head;
96
    }
97
98
    /**
99
     * Render image resized.
100
     *
101
     * @throws \Throwable
102
     */
103
    public function render(int $size): void
104
    {
105
        if ($size < self::MIN_SIZE || $size > self::MAX_SIZE) {
106
            $size = 256;
107
        }
108
109
        if (!$this->verifyCachedFile()) {
110
            $this->renderFullSize();
111
        } else {
112
            $this->createFromFile();
113
        }
114
115
        if ($size !== self::MAX_SIZE) {
116
            $this->head->resizeImage($size, $size, \Imagick::FILTER_LANCZOS2, 0.9);
117
        }
118
    }
119
120
    /**
121
     * Get ImagickPixel transparent object.
122
     */
123
    final protected function getImagickPixelTransparent(): \ImagickPixel
124
    {
125
        return new \ImagickPixel('transparent');
126
    }
127
128
    /**
129
     * Render Isometric from avatar sections.
130
     *
131
     * @throws \Throwable
132
     */
133
    protected function renderFullSize(): void
134
    {
135
        // Create Avatar Object
136
        $avatar = new Avatar($this->skinPath);
137
138
        // Face
139
        $avatar->render(self::HEAD_BASE_SIZE, Side::FRONT);
140
141
        $face = new \Imagick();
142
        $face->readImageBlob((string) $avatar);
143
        $face->brightnessContrastImage(8, 8);
144
        $face->setImageVirtualPixelMethod(\Imagick::VIRTUALPIXELMETHOD_TRANSPARENT);
145
        $face->setBackgroundColor(
146
            $this->getImagickPixelTransparent()
147
        );
148
149
        $face->distortImage(
150
            \Imagick::DISTORTION_AFFINE,
151
            $this->getFrontPoints(),
152
            true
153
        );
154
155
        // Top
156
        $avatar->render(self::HEAD_BASE_SIZE, Side::TOP);
157
158
        $top = new \Imagick();
159
        $top->readImageBlob((string) $avatar);
160
        $top->brightnessContrastImage(6, 6);
161
        $top->setImageVirtualPixelMethod(\Imagick::VIRTUALPIXELMETHOD_TRANSPARENT);
162
        $top->setBackgroundColor(
163
            $this->getImagickPixelTransparent()
164
        );
165
166
        $top->distortImage(
167
            \Imagick::DISTORTION_AFFINE,
168
            $this->getTopPoints(),
169
            true
170
        );
171
172
        // Right
173
        $avatar->render(self::HEAD_BASE_SIZE, Side::RIGHT);
174
175
        $right = new \Imagick();
176
        $right->readImageBlob((string) $avatar);
177
        $right->brightnessContrastImage(4, 4);
178
179
        $right->setImageVirtualPixelMethod(\Imagick::VIRTUALPIXELMETHOD_TRANSPARENT);
180
        $right->setBackgroundColor(
181
            $this->getImagickPixelTransparent()
182
        );
183
184
        $right->distortImage(
185
            \Imagick::DISTORTION_AFFINE,
186
            $this->getRightPoints(),
187
            true
188
        );
189
190
        // Head image
191
        $doubleAvatarSize = self::HEAD_BASE_SIZE * 2;
192
        $finalImageSize = $doubleAvatarSize + (self::HEAD_MARGIN * 2);
193
194
        $this->head = new \Imagick();
195
        $this->head->newImage($finalImageSize, $finalImageSize, $this->getImagickPixelTransparent());
196
197
        // This is weird, but it works
198
        $faceX = ((int) round($doubleAvatarSize / 2)) - 2 + self::HEAD_MARGIN;
199
        $faceY = $rightY = ((int) round($doubleAvatarSize / 4)) - 1 + self::HEAD_MARGIN;
200
        $topX = $rightX = ((int) round($doubleAvatarSize / 16)) + self::HEAD_MARGIN;
201
        $topY = -1 + self::HEAD_MARGIN;
202
203
        // Add Face Section
204
        $this->head->compositeimage($face->getimage(), \Imagick::COMPOSITE_PLUS, $faceX, $faceY);
205
206
        // Add Top Section
207
        $this->head->compositeimage($top->getimage(), \Imagick::COMPOSITE_PLUS, $topX, $topY);
208
209
        // Add Right Section
210
        $this->head->compositeimage($right->getimage(), \Imagick::COMPOSITE_PLUS, $rightX, $rightY);
211
212
        // Set format to PNG
213
        $this->head->setImageFormat('png');
214
215
        $this->isometricPath = IsometricsStorage::getPath($this->uuid);
216
        $this->head->writeImage($this->isometricPath);
217
    }
218
219
    /**
220
     * Create $head Imagick Object from previously rendered head.
221
     *
222
     * @throws \ImagickException
223
     */
224
    protected function createFromFile(): void
225
    {
226
        $this->head = new \Imagick($this->isometricPath);
227
    }
228
229
    /**
230
     * Check cached file.
231
     */
232
    protected function verifyCachedFile(): bool
233
    {
234
        if (!$this->checkCacheStatusFlag) {
235
            return true;
236
        }
237
238
        if (!$this->isometricPath || (filemtime($this->isometricPath) <= $this->lastUpdate)) {
239
            return false;
240
        }
241
242
        return true;
243
    }
244
245
    /**
246
     * Point for face section.
247
     *
248
     * @param int $size
249
     */
250
    private function getFrontPoints($size = self::HEAD_BASE_SIZE): array
251
    {
252
        $cosine_result = round(self::COSINE_PI_6 * $size);
253
        $half_size = round($size / 2);
254
255
        return [
256
            0, 0, 0, 0,
257
            0, $size, 0, $size,
258
            $size, 0, -$cosine_result, $half_size,
259
        ];
260
    }
261
262
    /**
263
     * Points for top section.
264
     *
265
     * @param int $size
266
     */
267
    private function getTopPoints($size = self::HEAD_BASE_SIZE): array
268
    {
269
        $cosine_result = round(self::COSINE_PI_6 * $size);
270
        $half_size = round($size / 2);
271
272
        return [
273
            0, $size, 0, 0,
274
            0, 0, -$cosine_result, -$half_size,
275
            $size, $size, $cosine_result, -$half_size,
276
        ];
277
    }
278
279
    /**
280
     * Points for right section.
281
     *
282
     * @param int $size
283
     */
284
    private function getRightPoints($size = self::HEAD_BASE_SIZE): array
285
    {
286
        $cosine_result = round(self::COSINE_PI_6 * $size);
287
        $half_size = round($size / 2);
288
289
        return [
290
            $size, 0, 0, 0,
291
            0, 0, -$cosine_result, -$half_size,
292
            $size, $size, 0, $size,
293
        ];
294
    }
295
}
296