@kirigami/php-prepros 1.1.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1593 -869
- package/index.d.ts +145 -2
- package/index.js +1 -1
- package/package.json +4 -4
- package/src/imagebatch.php +67 -0
- package/src/libraries/aliases.inc.php +836 -0
- package/src/libraries/curl.class.php +20 -3
- package/src/libraries/fs.class.php +161 -6
- package/src/libraries/html.class.php +92 -22
- package/src/libraries/img.class.php +372 -17
- package/src/libraries/ld.class.php +804 -0
- package/src/libraries/md.class.php +502 -173
- package/src/libraries/md.plugins.php +25 -19
- package/src/libraries/normalizer.class.php +310 -0
- package/src/libraries/prepros.class.php +149 -10
- package/src/libraries/prepros.plugins.php +56 -6
- package/src/libraries/schema.class.php +550 -0
- package/src/libraries/scraper.class.php +64 -59
- package/src/libraries/std.class.php +3 -0
- package/src/libraries/str.class.php +50 -16
- package/src/libraries/yaml.class.php +66 -66
- package/src/phpjs/fstat.js +9 -0
- package/src/prepros.js +126 -47
- package/src/prepros.php +9 -2
- package/src/runenv.php +5 -2
- package/src/utils/getfilestats.js +30 -0
- package/src/utils/isbinary.js +14 -14
- package/src/utils.inc.php +20 -9
|
@@ -1,9 +1,26 @@
|
|
|
1
1
|
<?php
|
|
2
2
|
|
|
3
|
+
if (!defined('IMAGETYPE_AVIF')) define('IMAGETYPE_AVIF', 19);
|
|
3
4
|
|
|
4
5
|
class IMG
|
|
5
6
|
{
|
|
6
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Default raster size (in pixels) used for the longest side when
|
|
10
|
+
* rasterizing vector formats (SVG, EPS, AI, PDF) via Imagick. Vector
|
|
11
|
+
* files are resolution-independent, and GD only ever deals in pixels,
|
|
12
|
+
* so a fallback size is needed for files that don't declare their own
|
|
13
|
+
* explicit pixel dimensions -- otherwise the underlying delegate
|
|
14
|
+
* library (librsvg, ghostscript...) may rasterize at a tiny default
|
|
15
|
+
* (often 100x100). The other side is scaled to preserve the aspect
|
|
16
|
+
* ratio reported by the file (explicit width/height, or a viewBox);
|
|
17
|
+
* a file that does declare explicit dimensions is honored as-is.
|
|
18
|
+
*/
|
|
19
|
+
private const VECTOR_DEFAULT_SIZE = 2000;
|
|
20
|
+
|
|
21
|
+
/** File extensions treated as vector formats for VECTOR_DEFAULT_SIZE purposes. */
|
|
22
|
+
private const VECTOR_EXTENSIONS = ['svg', 'eps', 'ai', 'pdf'];
|
|
23
|
+
|
|
7
24
|
private GdImage|null $im = null;
|
|
8
25
|
private array|null $info = null;
|
|
9
26
|
|
|
@@ -13,18 +30,99 @@ class IMG
|
|
|
13
30
|
if (!is_file($file) || !is_readable($file)) throw new Exception("Source file unreadable.");
|
|
14
31
|
|
|
15
32
|
$this->info = @getimagesize($file);
|
|
16
|
-
if (!$this->info) throw new Exception("Invalid image file.");
|
|
17
33
|
|
|
18
|
-
|
|
19
|
-
|
|
34
|
+
if ($this->info) {
|
|
35
|
+
[$srcW, $srcH, $type] = $this->info;
|
|
36
|
+
if ($srcW <= 0 || $srcH <= 0) throw new Exception("Invalid image file.");
|
|
20
37
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
38
|
+
$this->im = match ($type) {
|
|
39
|
+
IMAGETYPE_JPEG => @imagecreatefromjpeg($file),
|
|
40
|
+
IMAGETYPE_PNG => @imagecreatefrompng($file),
|
|
41
|
+
IMAGETYPE_GIF => @imagecreatefromgif($file),
|
|
42
|
+
IMAGETYPE_WEBP => (function_exists('imagecreatefromwebp') ? @imagecreatefromwebp($file) : null),
|
|
43
|
+
IMAGETYPE_AVIF => (function_exists('imagecreatefromavif') ? @imagecreatefromavif($file) : null),
|
|
44
|
+
default => null,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Fall back to Imagick for anything GD can't handle: a type GD
|
|
49
|
+
// doesn't recognize at all, a format GD was compiled without
|
|
50
|
+
// support for, or a format getimagesize() itself can't parse
|
|
51
|
+
// (e.g. HEIC, TIFF, BMP).
|
|
52
|
+
if (!$this->im) {
|
|
53
|
+
$this->im = $this->loadFromImagick($file);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!$this->im) throw new Exception("Image format not supported or GD extension missing support for this format.");
|
|
57
|
+
|
|
58
|
+
// getimagesize() failed but Imagick managed to decode the file:
|
|
59
|
+
// rebuild a minimal $info from the resulting GD image so the rest
|
|
60
|
+
// of the class (which reads $this->info[2]) keeps working.
|
|
61
|
+
if (!$this->info) {
|
|
62
|
+
$this->info = [imagesx($this->im), imagesy($this->im), IMAGETYPE_PNG];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Loads an image via Imagick and bridges it back to a GD resource.
|
|
69
|
+
* Used as a fallback for formats GD itself cannot decode (HEIC, TIFF,
|
|
70
|
+
* BMP, or any format missing from the compiled GD build). Imagick
|
|
71
|
+
* converts the image to a PNG blob in memory (preserving alpha), and
|
|
72
|
+
* GD then decodes that blob normally; nothing is written to disk.
|
|
73
|
+
*/
|
|
74
|
+
private function loadFromImagick(string $file): ?GdImage
|
|
75
|
+
{
|
|
76
|
+
if (!class_exists('Imagick')) return null;
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
$imagick = new Imagick();
|
|
80
|
+
|
|
81
|
+
$ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));
|
|
82
|
+
if (in_array($ext, self::VECTOR_EXTENSIONS, true)) {
|
|
83
|
+
// Vector formats have no intrinsic pixel size. Ping first
|
|
84
|
+
// to read whatever aspect ratio the file/delegate reports
|
|
85
|
+
// (an explicit width/height, or an SVG viewBox), then
|
|
86
|
+
// rasterize at VECTOR_DEFAULT_SIZE on the longest side
|
|
87
|
+
// while keeping that ratio, instead of forcing a square
|
|
88
|
+
// that could distort the image.
|
|
89
|
+
$probe = new Imagick();
|
|
90
|
+
$probe->pingImage($file);
|
|
91
|
+
$pw = $probe->getImageWidth();
|
|
92
|
+
$ph = $probe->getImageHeight();
|
|
93
|
+
$probe->clear();
|
|
94
|
+
|
|
95
|
+
if ($pw > 0 && $ph > 0) {
|
|
96
|
+
$scale = self::VECTOR_DEFAULT_SIZE / max($pw, $ph);
|
|
97
|
+
$targetW = max(1, (int) round($pw * $scale));
|
|
98
|
+
$targetH = max(1, (int) round($ph * $scale));
|
|
99
|
+
} else {
|
|
100
|
+
// Nothing usable to infer a ratio from: fall back to
|
|
101
|
+
// a square canvas.
|
|
102
|
+
$targetW = $targetH = self::VECTOR_DEFAULT_SIZE;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
$imagick->setBackgroundColor(new ImagickPixel('transparent'));
|
|
106
|
+
$imagick->setSize($targetW, $targetH);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
$imagick->readImage($file);
|
|
110
|
+
|
|
111
|
+
// If the source has multiple frames/pages (e.g. an animated
|
|
112
|
+
// HEIC sequence, or a multi-page PDF), keep only the first
|
|
113
|
+
// one, matching how GD would have loaded a static image.
|
|
114
|
+
if ($imagick->getNumberImages() > 1) {
|
|
115
|
+
$imagick->setIteratorIndex(0);
|
|
116
|
+
}
|
|
117
|
+
$imagick->setImageFormat('png32'); // 32-bit PNG, keeps alpha channel
|
|
118
|
+
$blob = $imagick->getImageBlob();
|
|
119
|
+
$imagick->clear();
|
|
120
|
+
} catch (\Throwable $e) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
$im = @imagecreatefromstring($blob);
|
|
125
|
+
return $im ?: null;
|
|
28
126
|
}
|
|
29
127
|
|
|
30
128
|
|
|
@@ -49,6 +147,13 @@ class IMG
|
|
|
49
147
|
|
|
50
148
|
public function resize(int $width, int $height = 0, bool $cover = false)
|
|
51
149
|
{
|
|
150
|
+
// Never upscale. The AVIF encoder (and, less visibly, the others) can
|
|
151
|
+
// choke on an enlarged raster, and blowing pixels up gains nothing —
|
|
152
|
+
// so a target larger than the source is clamped down to it rather than
|
|
153
|
+
// throwing an opaque encode error. Ask for sizes <= the source.
|
|
154
|
+
if ($width > $this->width) $width = $this->width;
|
|
155
|
+
if ($height > $this->height) $height = $this->height;
|
|
156
|
+
|
|
52
157
|
$srcRatio = $this->width / $this->height;
|
|
53
158
|
|
|
54
159
|
if (!$height) {
|
|
@@ -90,7 +195,7 @@ class IMG
|
|
|
90
195
|
private function prepare(int $x, int $y): GdImage
|
|
91
196
|
{
|
|
92
197
|
$img = imagecreatetruecolor($x, $y);
|
|
93
|
-
$hasAlpha = in_array($this->info[2], [IMAGETYPE_PNG, IMAGETYPE_WEBP, IMAGETYPE_GIF], true);
|
|
198
|
+
$hasAlpha = in_array($this->info[2], [IMAGETYPE_PNG, IMAGETYPE_WEBP, IMAGETYPE_GIF, IMAGETYPE_AVIF], true);
|
|
94
199
|
if ($hasAlpha) {
|
|
95
200
|
imagealphablending($img, false);
|
|
96
201
|
imagesavealpha($img, true);
|
|
@@ -104,19 +209,269 @@ class IMG
|
|
|
104
209
|
}
|
|
105
210
|
|
|
106
211
|
|
|
107
|
-
|
|
212
|
+
/**
|
|
213
|
+
* Converts an sRGB color (0-255) to Lab (D65), a perceptually uniform
|
|
214
|
+
* color space: a Euclidean distance in Lab roughly matches a color
|
|
215
|
+
* difference as perceived by the eye, unlike RGB where two colors at
|
|
216
|
+
* the same numeric "distance" can look very different or identical
|
|
217
|
+
* depending on the hue.
|
|
218
|
+
*/
|
|
219
|
+
private static function rgbToLab(int $r, int $g, int $b): array
|
|
220
|
+
{
|
|
221
|
+
$toLinear = fn(float $c) => ($c /= 255) <= 0.04045 ? $c / 12.92 : (($c + 0.055) / 1.055) ** 2.4;
|
|
222
|
+
[$rl, $gl, $bl] = [$toLinear($r), $toLinear($g), $toLinear($b)];
|
|
223
|
+
|
|
224
|
+
// Linear RGB -> XYZ (D65), then normalization against the white point.
|
|
225
|
+
$x = ($rl * 0.4124564 + $gl * 0.3575761 + $bl * 0.1804375) / 0.95047;
|
|
226
|
+
$y = $rl * 0.2126729 + $gl * 0.7151522 + $bl * 0.0721750;
|
|
227
|
+
$z = ($rl * 0.0193339 + $gl * 0.1191920 + $bl * 0.9503041) / 1.08883;
|
|
228
|
+
|
|
229
|
+
$f = fn(float $t) => $t > 0.008856 ? $t ** (1 / 3) : (7.787 * $t) + (16 / 116);
|
|
230
|
+
[$fx, $fy, $fz] = [$f($x), $f($y), $f($z)];
|
|
231
|
+
|
|
232
|
+
return [(116 * $fy) - 16, 500 * ($fx - $fy), 200 * ($fy - $fz)]; // [L, a, b]
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Extracts the most representative colors from the image.
|
|
238
|
+
*
|
|
239
|
+
* Algorithm: median-cut quantization (imagetruecolortopalette, the
|
|
240
|
+
* same family of technique as Imagick::quantizeImage), heavily
|
|
241
|
+
* over-sampling the number of requested colors, followed by
|
|
242
|
+
* perceptual post-processing in Lab space:
|
|
243
|
+
* - merging colors that are too close (CIE76 distance in Lab): the
|
|
244
|
+
* quantization often produces several hues that look virtually
|
|
245
|
+
* identical to the eye, which we don't want as separate entries;
|
|
246
|
+
* - excluding near-pure white/black via the L lightness component
|
|
247
|
+
* (reliable regardless of hue, unlike a threshold on raw r/g/b).
|
|
248
|
+
*
|
|
249
|
+
* @param int $numColors Number of colors to return.
|
|
250
|
+
* @param float $mergeTolerance Lab distance below which two colors are merged (0-100, ~6-10 = "near identical").
|
|
251
|
+
* @param bool $excludeNearWhiteAndBlack Excludes near-white/near-black colors.
|
|
252
|
+
* @param float $lightnessThreshold Lab lightness threshold (0-100) above/below which a color is considered near-white/near-black.
|
|
253
|
+
* @return string[] Colors as "#rrggbb", sorted by descending frequency.
|
|
254
|
+
*/
|
|
255
|
+
public function getRepresentativeColors(
|
|
256
|
+
int $numColors = 5,
|
|
257
|
+
float $mergeTolerance = 8.0,
|
|
258
|
+
bool $excludeNearWhiteAndBlack = true,
|
|
259
|
+
float $lightnessThreshold = 8.0
|
|
260
|
+
): array {
|
|
261
|
+
$srcW = $this->width;
|
|
262
|
+
$srcH = $this->height;
|
|
263
|
+
|
|
264
|
+
// Analyzing at full resolution brings nothing for this kind of
|
|
265
|
+
// extraction and is expensive in compute time.
|
|
266
|
+
$maxDim = 150;
|
|
267
|
+
$scale = min(1, $maxDim / max($srcW, $srcH));
|
|
268
|
+
$w = max(1, (int) round($srcW * $scale));
|
|
269
|
+
$h = max(1, (int) round($srcH * $scale));
|
|
270
|
+
|
|
271
|
+
// Flatten onto a white background (as for JPEG export): otherwise
|
|
272
|
+
// the transparent areas of PNG/WEBP images would default to being
|
|
273
|
+
// counted as black on a truecolor canvas.
|
|
274
|
+
$sample = imagecreatetruecolor($w, $h);
|
|
275
|
+
imagealphablending($sample, true);
|
|
276
|
+
$white = imagecolorallocate($sample, 255, 255, 255);
|
|
277
|
+
imagefilledrectangle($sample, 0, 0, $w, $h, $white);
|
|
278
|
+
imagecopyresampled($sample, $this->im, 0, 0, 0, 0, $w, $h, $srcW, $srcH);
|
|
279
|
+
|
|
280
|
+
// We heavily over-sample the number of requested colors: this
|
|
281
|
+
// leaves enough room for the perceptual merge and the white/black
|
|
282
|
+
// exclusion below without running short of colors.
|
|
283
|
+
$buckets = max($numColors * 6, 24);
|
|
284
|
+
imagetruecolortopalette($sample, false, $buckets);
|
|
285
|
+
|
|
286
|
+
// Count occurrences of each color in the generated palette.
|
|
287
|
+
$counts = array_fill(0, imagecolorstotal($sample), 0);
|
|
288
|
+
for ($y = 0; $y < $h; $y++) {
|
|
289
|
+
for ($x = 0; $x < $w; $x++) {
|
|
290
|
+
$counts[imagecolorat($sample, $x, $y)]++;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
$entries = [];
|
|
295
|
+
foreach ($counts as $index => $count) {
|
|
296
|
+
if ($count === 0) continue;
|
|
297
|
+
$rgb = imagecolorsforindex($sample, $index);
|
|
298
|
+
$lab = self::rgbToLab($rgb['red'], $rgb['green'], $rgb['blue']);
|
|
299
|
+
|
|
300
|
+
if ($excludeNearWhiteAndBlack && ($lab[0] > 100 - $lightnessThreshold || $lab[0] < $lightnessThreshold)) {
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
$entries[] = ['r' => $rgb['red'], 'g' => $rgb['green'], 'b' => $rgb['blue'], 'lab' => $lab, 'count' => $count];
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
usort($entries, fn($a, $b) => $b['count'] - $a['count']);
|
|
308
|
+
|
|
309
|
+
// Merge perceptually close colors by grouping their occurrences,
|
|
310
|
+
// always starting from the most frequent one.
|
|
311
|
+
$merged = [];
|
|
312
|
+
foreach ($entries as $entry) {
|
|
313
|
+
foreach ($merged as &$cluster) {
|
|
314
|
+
$dl = $cluster['lab'][0] - $entry['lab'][0];
|
|
315
|
+
$da = $cluster['lab'][1] - $entry['lab'][1];
|
|
316
|
+
$db = $cluster['lab'][2] - $entry['lab'][2];
|
|
317
|
+
if (sqrt($dl * $dl + $da * $da + $db * $db) <= $mergeTolerance) {
|
|
318
|
+
$cluster['count'] += $entry['count'];
|
|
319
|
+
continue 2;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
unset($cluster);
|
|
323
|
+
$merged[] = $entry;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
usort($merged, fn($a, $b) => $b['count'] - $a['count']);
|
|
327
|
+
|
|
328
|
+
return array_map(
|
|
329
|
+
fn($c) => sprintf('#%02x%02x%02x', $c['r'], $c['g'], $c['b']),
|
|
330
|
+
array_slice($merged, 0, $numColors)
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* The AVIF encoder (libavif/aom) used by imageavif() can fail with
|
|
337
|
+
* "Encoding of color planes failed" in two common cases:
|
|
338
|
+
* - odd width/height (4:2:0 chroma subsampling constraint)
|
|
339
|
+
* - image too large, causing the encoder to run out of memory for
|
|
340
|
+
* its internal buffers ("aom_codec_encode: Failed to allocate lag buffers")
|
|
341
|
+
* We fix both before encoding, on a temporary copy, without modifying
|
|
342
|
+
* $this->im.
|
|
343
|
+
*/
|
|
344
|
+
private function prepareForAvif(GdImage $im, int $maxDimension = 4000): GdImage
|
|
345
|
+
{
|
|
346
|
+
$w = imagesx($im);
|
|
347
|
+
$h = imagesy($im);
|
|
348
|
+
|
|
349
|
+
// Cap the resolution if needed (encoder memory protection).
|
|
350
|
+
if ($w > $maxDimension || $h > $maxDimension) {
|
|
351
|
+
$scale = min($maxDimension / $w, $maxDimension / $h);
|
|
352
|
+
$newW = max(1, (int) round($w * $scale));
|
|
353
|
+
$newH = max(1, (int) round($h * $scale));
|
|
354
|
+
$resized = imagecreatetruecolor($newW, $newH);
|
|
355
|
+
imagealphablending($resized, false);
|
|
356
|
+
imagesavealpha($resized, true);
|
|
357
|
+
$transparent = imagecolorallocatealpha($resized, 0, 0, 0, 127);
|
|
358
|
+
imagefilledrectangle($resized, 0, 0, $newW, $newH, $transparent);
|
|
359
|
+
imagecopyresampled($resized, $im, 0, 0, 0, 0, $newW, $newH, $w, $h);
|
|
360
|
+
$im = $resized;
|
|
361
|
+
$w = $newW;
|
|
362
|
+
$h = $newH;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Force even dimensions.
|
|
366
|
+
$w2 = $w % 2 ? $w + 1 : $w;
|
|
367
|
+
$h2 = $h % 2 ? $h + 1 : $h;
|
|
368
|
+
if ($w2 !== $w || $h2 !== $h) {
|
|
369
|
+
$padded = imagecreatetruecolor($w2, $h2);
|
|
370
|
+
imagealphablending($padded, false);
|
|
371
|
+
imagesavealpha($padded, true);
|
|
372
|
+
$transparent = imagecolorallocatealpha($padded, 0, 0, 0, 127);
|
|
373
|
+
imagefilledrectangle($padded, 0, 0, $w2, $h2, $transparent);
|
|
374
|
+
imagecopy($padded, $im, 0, 0, 0, 0, $w, $h);
|
|
375
|
+
$im = $padded;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return $im;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Encodes to AVIF with automatic fallback: if encoding fails (memory
|
|
384
|
+
* error or another internal aom issue), retry with a higher speed
|
|
385
|
+
* (less memory-hungry), then with a reduced resolution. Throws an
|
|
386
|
+
* explicit exception if everything fails, instead of letting a silent
|
|
387
|
+
* PHP warning through and leaving a corrupted/missing file behind.
|
|
388
|
+
*/
|
|
389
|
+
private function encodeAvif(GdImage $im, string $dest, int $quality = 82): bool
|
|
390
|
+
{
|
|
391
|
+
$attempts = [
|
|
392
|
+
['maxDimension' => 4000, 'speed' => 4],
|
|
393
|
+
['maxDimension' => 4000, 'speed' => 8],
|
|
394
|
+
['maxDimension' => 2000, 'speed' => 8],
|
|
395
|
+
];
|
|
396
|
+
|
|
397
|
+
foreach ($attempts as $attempt) {
|
|
398
|
+
$prepared = $this->prepareForAvif($im, $attempt['maxDimension']);
|
|
399
|
+
$ok = @imageavif($prepared, $dest, $quality, $attempt['speed']);
|
|
400
|
+
if ($ok && is_file($dest) && filesize($dest) > 0) return true;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Encodes the current image to $dest, the format taken from the file
|
|
409
|
+
* extension (jpg/jpeg, png, gif, webp, avif).
|
|
410
|
+
*
|
|
411
|
+
* $quality (0-100) applies to the lossy formats (jpg, webp, avif); it is
|
|
412
|
+
* ignored for png (whose second arg is a 0-9 compression level) and gif.
|
|
413
|
+
* When null, each format keeps its own default (82 for jpg/webp/avif).
|
|
414
|
+
*/
|
|
415
|
+
public function save(string $dest, ?int $quality = null): self
|
|
108
416
|
{
|
|
109
417
|
$ext = strtolower(pathinfo($dest, PATHINFO_EXTENSION));
|
|
110
418
|
$dir = pathinfo($dest, PATHINFO_DIRNAME);
|
|
111
419
|
if (!is_dir($dir) && !@mkdir($dir, 0777, true)) throw new Exception("Invalid destination.");
|
|
112
|
-
match ($ext) {
|
|
113
|
-
'jpg', 'jpeg' => imagejpeg($this->im, $dest,
|
|
420
|
+
$ok = match ($ext) {
|
|
421
|
+
'jpg', 'jpeg' => imagejpeg($this->im, $dest, $quality ?? 82),
|
|
114
422
|
'png' => imagepng($this->im, $dest, 6),
|
|
115
423
|
'gif' => imagegif($this->im, $dest),
|
|
116
|
-
'webp' => (function_exists('imagewebp') ? imagewebp($this->im, $dest,
|
|
424
|
+
'webp' => (function_exists('imagewebp') ? imagewebp($this->im, $dest, $quality ?? 82) : false),
|
|
425
|
+
'avif' => (function_exists('imageavif') ? $this->encodeAvif($this->im, $dest, $quality ?? 82) : false),
|
|
117
426
|
default => throw new Exception("Invalid output file type.")
|
|
118
427
|
};
|
|
119
|
-
|
|
428
|
+
if (!$ok) throw new Exception("Failed to encode image as '{$ext}'.");
|
|
429
|
+
PREPROS::exportFile(realpath($dest));
|
|
120
430
|
return $this;
|
|
121
431
|
}
|
|
122
|
-
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
public static function asset(string $path, $width = 0, $height = 0, $cover = false, $backtrace = '')
|
|
435
|
+
{
|
|
436
|
+
$srcfile = FS::pathJoin(PREPROS::$config->image->source, $path);
|
|
437
|
+
if(!$srcinfo = PREPROS::fstat($srcfile)) throw new Exception("Invalid image file.");
|
|
438
|
+
|
|
439
|
+
$suffix = '';
|
|
440
|
+
if ($width && $height) $suffix = $cover ? '-' . $width . 'x' . $height . '-cover' : '-' . $width . 'x' . $height;
|
|
441
|
+
elseif ($width) $suffix = '-' . $width . 'w';
|
|
442
|
+
elseif ($height) $suffix = '-' . $height . 'h';
|
|
443
|
+
$destname = preg_replace('/\.[a-z]{2,4}+$/i', '', $path) . $suffix . '.' . PREPROS::$config->image->format;
|
|
444
|
+
$destfile = FS::pathJoin(PREPROS::$config->data->root, PREPROS::$config->image->dest, $destname);
|
|
445
|
+
$destinfo = PREPROS::fstat($destfile);
|
|
446
|
+
|
|
447
|
+
if(!$destinfo || ((strtotime($srcinfo->modifiedAt) - strtotime($destinfo->modifiedAt)) > 10)) {
|
|
448
|
+
if(!$localfile = current(PREPROS::mount($srcfile))) throw new Exception("Can't mount image.");
|
|
449
|
+
if(!is_file($localfile)) throw new Exception("Can't mount image.");
|
|
450
|
+
$img = new self($localfile);
|
|
451
|
+
if ($width && $height) $img->resize($width, $height, $cover);
|
|
452
|
+
elseif ($width) $img->resize($width);
|
|
453
|
+
elseif ($height) $img->resize((int) round($img->width * $height / $img->height), $height);
|
|
454
|
+
$img->save(FS::pathJoin('/project', $destfile));
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if(!$backtrace) $backtrace = PREPROS::backtraceFile();
|
|
458
|
+
return FS::getRelativePath($backtrace, FS::pathJoin('/project', $destfile));
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
public static function palette(string $path, $colors = 5)
|
|
463
|
+
{
|
|
464
|
+
$srcfile = FS::pathJoin(PREPROS::$config->image->source, $path);
|
|
465
|
+
if(!$srcinfo = PREPROS::fstat($srcfile)) throw new Exception("Invalid image file.");
|
|
466
|
+
$key = 'palette_' . STR::shorthash("{$srcfile}:{$srcinfo->modifiedAt}:{$colors}");
|
|
467
|
+
if($palette = CACHE::get($key)) return $palette;
|
|
468
|
+
if(!$localfile = current(PREPROS::mount($srcfile))) throw new Exception("Can't mount image.");
|
|
469
|
+
if(!$palette = (new self($localfile))->getRepresentativeColors($colors)) throw new Exception("Can't extract palette from image \"{$path}\".");
|
|
470
|
+
CACHE::set($key, $palette);
|
|
471
|
+
return $palette;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
}
|