1<?php
2
3// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
4// See the LICENCE file in the repository root for full licence text.
5
6namespace App\Libraries;
7
8use App\Exceptions\ImageProcessorException;
9
10class ImageProcessor
11{
12 public $errors = [];
13
14 public $hardMaxDim = [5000, 5000];
15 public $hardMaxFileSize = 10000000;
16 public $allowedTypes = [IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_PNG];
17
18 public $inputDim = null;
19 public $inputFileSize = null;
20 public $inputPath = null;
21 public $targetDim = null;
22 public $targetFileSize = null;
23
24 public function __construct($inputPath, $targetDim, $targetFileSize)
25 {
26 $this->inputPath = $inputPath;
27 $this->targetDim = $targetDim;
28 $this->targetFileSize = $targetFileSize;
29
30 $this->parseInput();
31 }
32
33 public function basicCheck()
34 {
35 if ($this->inputFileSize > $this->hardMaxFileSize) {
36 throw new ImageProcessorException(osu_trans('users.show.edit.cover.upload.too_large'));
37 }
38
39 if ($this->inputDim === null || !in_array($this->inputDim[2], $this->allowedTypes, true)) {
40 throw new ImageProcessorException(osu_trans('users.show.edit.cover.upload.unsupported_format'));
41 }
42
43 if ($this->inputDim[0] > $this->hardMaxDim[0] || $this->inputDim[1] > $this->hardMaxDim[1]) {
44 throw new ImageProcessorException(osu_trans('users.show.edit.cover.upload.too_large'));
45 }
46 }
47
48 public function ext()
49 {
50 return image_type_to_extension($this->inputDim[2], false);
51 }
52
53 public function parseInput()
54 {
55 $this->inputDim = read_image_properties($this->inputPath);
56 $this->inputFileSize = filesize($this->inputPath);
57 }
58
59 public function purgeExif()
60 {
61 if ($this->inputDim[2] !== IMAGETYPE_JPEG) {
62 return;
63 }
64
65 exec('jhead -autorot -purejpg -q '.escapeshellarg($this->inputPath));
66 $this->parseInput();
67 }
68
69 public function process()
70 {
71 $this->basicCheck();
72
73 $this->purgeExif();
74
75 $inputImage = open_image($this->inputPath, $this->inputDim);
76
77 if ($inputImage === null) {
78 throw new ImageProcessorException(osu_trans('users.show.edit.cover.upload.broken_file'));
79 }
80
81 if (
82 $this->inputDim[0] <= $this->targetDim[0] &&
83 $this->inputDim[1] <= $this->targetDim[1]
84 ) {
85 if ($this->inputFileSize < $this->targetFileSize) {
86 return;
87 }
88
89 $image = $inputImage;
90 } else {
91 $start = [0, 0];
92 $inDim = [$this->inputDim[0], $this->inputDim[1]];
93 $outDim = [$this->targetDim[0], $this->targetDim[1]];
94
95 // figure out how to crop.
96 if ($this->inputDim[0] / $this->inputDim[1] >= $this->targetDim[0] / $this->targetDim[1]) {
97 $inDim[0] = $this->targetDim[0] / $this->targetDim[1] * $this->inputDim[1];
98 $start[0] = ($this->inputDim[0] - $inDim[0]) / 2;
99 } else {
100 $inDim[1] = $this->targetDim[1] / $this->targetDim[0] * $this->inputDim[0];
101 $start[1] = ($this->inputDim[1] - $inDim[1]) / 2;
102 }
103
104 // don't scale if input image is smaller.
105 if ($inDim[0] < $outDim[0] || $inDim[1] < $outDim[1]) {
106 $outDim = $inDim;
107 }
108
109 $image = imagecreatetruecolor($outDim[0], $outDim[1]);
110 imagesavealpha($image, true);
111 imagefill($image, 0, 0, imagecolorallocatealpha($image, 0, 0, 0, 127));
112 imagecopyresampled($image, $inputImage, 0, 0, $start[0], $start[1], $outDim[0], $outDim[1], $inDim[0], $inDim[1]);
113 }
114
115 $toJpeg = true;
116
117 if ($this->inputDim[2] === IMAGETYPE_PNG || $this->inputDim[2] === IMAGETYPE_GIF) {
118 imagepng($image, $this->inputPath);
119
120 $this->parseInput();
121 $toJpeg = $this->inputFileSize > $this->targetFileSize;
122 }
123
124 if ($toJpeg) {
125 imagejpeg($image, $this->inputPath, 90);
126 }
127
128 $this->parseInput();
129 }
130}