forked from moodle/moodle
-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathai_image.php
382 lines (350 loc) · 14 KB
/
ai_image.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_ai;
use core\exception\moodle_exception;
/**
* AI Image.
*
* @package core_ai
* @copyright 2024 Huong Nguyen <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class ai_image {
/** @var array Image information. */
private array $imageinfo;
/** @var false|\GdImage Image object. */
private false|\GDImage $imgobject;
/**
* Constructor for the image processing class.
*
* Initializes the class with the provided image path, setting up the image object and its properties.
* The constructor checks if the GD library functions for PNG and JPEG are available, ensures the image file
* exists and is readable, and then creates an image resource object based on the file type (JPEG, PNG, or GIF).
*
* @param string $imagepath The path to the image file.
*/
public function __construct(
/** @var string Image path. */
private string $imagepath,
) {
ini_set('gd.jpeg_ignore_warning', 1);
if (!file_exists($imagepath) || !is_readable($imagepath)) {
throw new moodle_exception('invalidfile', debuginfo: $imagepath);
}
$imageinfo = getimagesize($imagepath);
if (empty($imageinfo)) {
throw new moodle_exception('invalidfile', debuginfo: $imagepath);
}
$this->imageinfo = $imageinfo;
switch ($this->imageinfo['mime']) {
case 'image/jpeg':
if (!function_exists('imagecreatefromjpeg')) {
throw new moodle_exception('gdfeaturenotsupported', a: 'jpeg');
}
$this->imgobject = imagecreatefromjpeg($imagepath);
break;
case 'image/png':
if (!function_exists('imagecreatefrompng')) {
throw new moodle_exception('gdfeaturenotsupported', a: 'png');
}
$this->imgobject = imagecreatefrompng($imagepath);
break;
case 'image/gif':
if (!function_exists('imagecreatefromgif')) {
throw new moodle_exception('gdfeaturenotsupported', a: 'gif');
}
$this->imgobject = imagecreatefromgif($imagepath);
break;
default:
throw new moodle_exception('gdmimetypenotsupported', debuginfo: $this->imageinfo['mime']);
}
}
/**
* Get the predominant color of a specific area of the image.
*
* This method analyzes a rectangular area of the image, calculating the
* average color by summing up the red, green, and blue components of all pixels
* within the area, and then dividing by the total number of pixels.
*
* @param int $x X coordinate of the top-left corner of the area.
* @param int $y Y coordinate of the top-left corner of the area.
* @param int $width Width of the area.
* @param int $height Height of the area.
* @return array RGB array of the predominant color, with keys 'red', 'green', and 'blue'.
*/
private function get_predominant_color(int $x, int $y, int $width, int $height): array {
// If the width or height is smaller than 10 pixels, sample the entire image.
if (imagesx($this->imgobject) < 10 || imagesy($this->imgobject) < 10) {
$x = 0;
$y = 0;
$width = imagesx($this->imgobject);
$height = imagesy($this->imgobject);
}
// Initialize variables to accumulate the total red, green, and blue values.
$redtotal = $greentotal = $bluetotal = 0;
// Initialize a counter for the number of pixels processed.
$pixelcount = 0;
// Iterate over each pixel within the specified area of the image.
for ($i = $x; $i < $x + $width; $i++) {
for ($j = $y; $j < $y + $height; $j++) {
// Retrieve the color index of the current pixel.
$rgb = imagecolorat(
image: $this->imgobject,
x: $i,
y: $j);
// Extract the red component (shift the bits 16 places to the right and mask the rest).
$red = ($rgb >> 16) & 0xFF;
// Extract the green component (shift the bits 8 places to the right and mask the rest).
$green = ($rgb >> 8) & 0xFF;
// Extract the blue component (mask directly to get the blue value).
$blue = $rgb & 0xFF;
// Accumulate the red, green, and blue values.
$redtotal += $red;
$greentotal += $green;
$bluetotal += $blue;
// Increment the pixel counter.
$pixelcount++;
}
}
// Calculate the average red, green, and blue values by dividing the total by the number of pixels.
return [
'red' => $redtotal / $pixelcount,
'green' => $greentotal / $pixelcount,
'blue' => $bluetotal / $pixelcount,
];
}
/**
* Determine if the color is dark based on its RGB values.
*
* This method uses a formula to calculate the luminance of a color.
* Luminance is a weighted sum of the red, green, and blue components, with green having the highest weight
* because the human eye is more sensitive to green.
* A luminance value below 128 is generally considered dark.
*
* @param array $color RGB array with keys 'red', 'green', and 'blue'.
* @return bool True if the color is dark, false if it is light.
*/
private function is_color_dark(array $color): bool {
// Calculate the luminance using the standard formula.
// Luminance = 0.299 * Red + 0.587 * Green + 0.114 * Blue.
// The coefficients correspond to the human eye's sensitivity to these colors.
$luminance = (0.299 * $color['red'] + 0.587 * $color['green'] + 0.114 * $color['blue']);
// Return true if the luminance is below 128 (dark), otherwise return false (light).
return $luminance < 128;
}
/**
* Draw a pill-shaped rounded rectangle.
* The pill is composed of two half circles and a single rectangle.
*
* @param int $x1 Top-left X coordinate of the rectangle.
* @param int $y1 Top-left Y coordinate of the rectangle.
* @param int $x2 Bottom-right X coordinate of the rectangle.
* @param int $y2 Bottom-right Y coordinate of the rectangle.
* @param int $radius Radius of the rounded corners (half the pill height).
* @param int $color Color for the pill background.
*/
private function draw_rounded_rectangle(
int $x1,
int $y1,
int $x2,
int $y2,
int $radius,
int $color
): void {
// Draw two half circles at the ends of the pill.
// Left half circle.
imagefilledarc(
image: $this->imgobject,
center_x: $x1 + $radius, // Center X coordinate.
center_y: ($y1 + $y2) / 2, // Center Y coordinate.
width: $radius * 2, // Width of the circle (diameter).
height: $radius * 2, // Height of the circle (diameter).
start_angle: 90,
end_angle: 270,
color: $color,
style: IMG_ARC_PIE
);
// Right half circle.
imagefilledarc(
image: $this->imgobject,
center_x: $x2 - $radius,
center_y: ($y1 + $y2) / 2,
width: $radius * 2,
height: $radius * 2,
start_angle: 270,
end_angle: 90,
color: $color,
style: IMG_ARC_PIE
);
// Draw the rectangle joining the two half circles.
imagefilledrectangle(
image: $this->imgobject,
x1: $x1 + $radius, // Start after the left half circle.
y1: $y1, // Top of the rectangle.
x2: $x2 - $radius, // End before the right half circle.
y2: $y2, // Bottom of the rectangle.
color: $color
);
}
/**
* Add watermark to image.
*
* @param string $watermark Watermark text.
* @param array $options Watermark options.
* @param array $pos Watermark position.
* @return $this
*/
public function add_watermark(
string $watermark = '',
array $options = [],
array $pos = [10, 10],
): static {
global $CFG;
if (empty($watermark)) {
$watermark = get_string('contentwatermark', 'core_ai');
}
if (empty($options)) {
$options = [
'font' => $CFG->libdir . '/default.ttf',
'fontsize' => '20',
'angle' => 0,
'ttf' => true,
];
}
$imagewidth = imagesx($this->imgobject);
$imageheight = imagesy($this->imgobject);
// Determine the size of the area to analyze: 10% of the image width and height.
$areawidth = (int)($imagewidth * 0.1);
$areaheight = (int)($imageheight * 0.1);
// Dynamically calculate the bottom-left corner coordinates.
$bottomleftcolor = $this->get_predominant_color(
x: 0,
y: $imageheight - $areaheight,
width: $areawidth,
height: $areaheight
);
// Set text color based on the background color.
if ($this->is_color_dark($bottomleftcolor)) {
$clr = imagecolorallocate( // White for dark background.
image: $this->imgobject,
red: 255,
green: 255,
blue: 255
);
$bgclr = imagecolorallocatealpha( // Black (80% transparent).
image: $this->imgobject,
red: 0,
green: 0,
blue: 0,
alpha: (int)(127 * 0.2)
);
} else {
$clr = imagecolorallocate( // Black for light background.
image: $this->imgobject,
red: 0,
green: 0,
blue: 0
);
$bgclr = imagecolorallocatealpha( // White (80% transparent).
image: $this->imgobject,
red: 255,
green: 255,
blue: 255,
alpha: (int)(127 * 0.2)
);
}
// Encode the text properly.
$text = iconv(
from_encoding: 'ISO-8859-8',
to_encoding: 'UTF-8',
string: $watermark
);
// Calculate text bounding box for determining pill siz), different for TTF and non-TTF fonts.
if (!empty($options['ttf'])) {
// For TTF fonts, use imagettfbbox to get the text's bounding box.
$bbox = imagettfbbox($options['fontsize'], $options['angle'], $options['font'], $text);
$textwidth = abs($bbox[4] - $bbox[0]);
$textheight = abs($bbox[5] - $bbox[1]);
} else {
// For non-TTF fonts, use imagefontwidth and imagefontheight.
$textwidth = strlen($text) * imagefontwidth($options['fontsize']);
$textheight = imagefontheight($options['fontsize']);
}
// Pill background dimensions.
$padding = 10;
$pillwidth = $textwidth + $padding * 2;
$pillheight = $textheight + $padding * 2;
// Position for the pill background.
$x = $pos[0];
$y = $imageheight - ($pos[1] + $pillheight); // Adjust Y based on the pill height.
// Draw the pill background.
$this->draw_rounded_rectangle(
x1: $x,
y1: $y,
x2: $x + $pillwidth,
y2: $y + $pillheight,
radius: $pillheight / 2,
color: $bgclr
);
// Correct the position of the text to center it inside the pill.
$textx = $x + (($pillwidth - $textwidth) / 2); // Center text horizontally in the pill.
$texty = $y + ((($pillheight - $textheight) / 2) * .75) + $textheight; // Center vertically, adjusting for baseline.
// Draw the text on top of the pill background.
if (!empty($options['ttf'])) {
imagettftext(
image: $this->imgobject,
size: $options['fontsize'],
angle: $options['angle'],
x: (int)$textx,
y: (int)$texty,
color: $clr,
font_filename: $options['font'],
text: $text,
);
} else {
imagestring(
image: $this->imgobject,
font: $options['fontsize'],
x: (int)$textx,
y: (int)$texty,
string: $text,
color: $clr,
);
}
return $this;
}
/**
* Save image.
*
* @param string $newpath New path to save image.
* @return bool Whether the save was successful
*/
public function save(string $newpath = ''): bool {
if (empty($newpath)) {
$newpath = $this->imagepath;
}
switch($this->imageinfo['mime']) {
case 'image/jpeg':
return imagejpeg(image: $this->imgobject, file: $newpath);
case 'image/png':
return imagepng(image: $this->imgobject, file: $newpath);
case 'image/gif':
return imagegif(image: $this->imgobject, file: $newpath);
default:
return false;
}
}
}