Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

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