Files
fitme3070.pt/config/functions_images.php
T
2026-06-12 21:20:23 +01:00

156 lines
6.8 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
// ─── Image optimization helpers ────────────────────────────────────────────────
// Resizes raster images IN PLACE so the longest side fits the per-area cap and
// re-encodes at web quality. Called on every upload (galerias, team,
// modalidades) and by the "Otimizar" buttons in the backoffice, which run the
// same pass over files uploaded before this existed.
//
// Idempotent by design: images already within the cap are left untouched, so
// the bulk buttons can be pressed repeatedly without generation loss.
// SVGs and animated GIFs are skipped (GD would rasterize/flatten them).
// Requires the GD extension; degrades to a no-op (status "failed") without it,
// so an upload never breaks because optimization couldn't run.
// Caps per area — longest side, in pixels.
const IMG_MAX_GALLERY = 1920;
const IMG_MAX_TEAM = 512;
const IMG_MAX_MODALIDADE = 256;
const IMG_MAX_EVENT = 1600;
const IMG_JPEG_QUALITY = 82;
const IMG_WEBP_QUALITY = 80;
// Resize one file in place. Returns ['status' => 'resized'|'skipped'|'failed',
// 'saved' => bytes freed (0 unless resized), 'reason' => short PT label].
function img_optimize(string $path, int $max_dim): array {
$skip = fn(string $why) => ['status' => 'skipped', 'saved' => 0, 'reason' => $why];
$fail = fn(string $why) => ['status' => 'failed', 'saved' => 0, 'reason' => $why];
if (!is_file($path)) return $fail('ficheiro não encontrado');
if (!extension_loaded('gd')) return $fail('extensão GD indisponível');
$info = @getimagesize($path);
if (!$info) return $skip('formato não suportado'); // SVG e outros não-raster
[$w, $h] = $info;
$type = $info[2];
$supported = [IMAGETYPE_JPEG, IMAGETYPE_PNG, IMAGETYPE_GIF];
if (function_exists('imagecreatefromwebp')) $supported[] = IMAGETYPE_WEBP;
if (!in_array($type, $supported, true)) return $skip('formato não suportado');
// GIFs animados perdiam a animação no GD — não tocar.
if ($type === IMAGETYPE_GIF
&& substr_count((string) file_get_contents($path), "\x21\xF9\x04") > 1) {
return $skip('GIF animado');
}
// Orientação EXIF: fotos de telemóvel guardadas "deitadas" contam para o
// lado maior depois de rodadas, por isso resolve-se antes do teste de tamanho.
$rotation = 0;
if ($type === IMAGETYPE_JPEG && function_exists('exif_read_data')) {
$orientation = (int) (@exif_read_data($path)['Orientation'] ?? 1);
$rotation = [3 => 180, 6 => -90, 8 => 90][$orientation] ?? 0;
if (abs($rotation) === 90) [$w, $h] = [$h, $w];
}
if ($w <= $max_dim && $h <= $max_dim && $rotation === 0) {
return $skip('já dentro do limite');
}
// Imagens grandes podem exceder o memory_limit ao descodificar.
if ($w * $h > 4_000_000) @ini_set('memory_limit', '512M');
$src = match ($type) {
IMAGETYPE_JPEG => @imagecreatefromjpeg($path),
IMAGETYPE_PNG => @imagecreatefrompng($path),
IMAGETYPE_GIF => @imagecreatefromgif($path),
IMAGETYPE_WEBP => @imagecreatefromwebp($path),
default => false,
};
if (!$src) return $fail('imagem ilegível');
if ($rotation !== 0) {
$rotated = imagerotate($src, $rotation, 0);
if ($rotated) { imagedestroy($src); $src = $rotated; }
$w = imagesx($src); $h = imagesy($src);
}
$scale = min(1, $max_dim / max($w, $h, 1));
$nw = max(1, (int) round($w * $scale));
$nh = max(1, (int) round($h * $scale));
$dst = imagecreatetruecolor($nw, $nh);
if ($type === IMAGETYPE_PNG || $type === IMAGETYPE_WEBP) {
imagealphablending($dst, false);
imagesavealpha($dst, true);
imagefilledrectangle($dst, 0, 0, $nw, $nh, imagecolorallocatealpha($dst, 0, 0, 0, 127));
} elseif ($type === IMAGETYPE_GIF) {
$tidx = imagecolortransparent($src);
if ($tidx >= 0 && $tidx < imagecolorstotal($src)) {
$t = imagecolorsforindex($src, $tidx);
$newt = imagecolorallocate($dst, $t['red'], $t['green'], $t['blue']);
imagefilledrectangle($dst, 0, 0, $nw, $nh, $newt);
imagecolortransparent($dst, $newt);
}
}
imagecopyresampled($dst, $src, 0, 0, 0, 0, $nw, $nh, $w, $h);
imagedestroy($src);
$tmp = $path . '.opt.tmp';
if ($type === IMAGETYPE_JPEG) imageinterlace($dst, true); // JPEG progressivo
$ok = match ($type) {
IMAGETYPE_JPEG => imagejpeg($dst, $tmp, IMG_JPEG_QUALITY),
IMAGETYPE_PNG => imagepng($dst, $tmp, 9),
IMAGETYPE_GIF => imagegif($dst, $tmp),
IMAGETYPE_WEBP => imagewebp($dst, $tmp, IMG_WEBP_QUALITY),
default => false,
};
imagedestroy($dst);
if (!$ok || !is_file($tmp)) { @unlink($tmp); return $fail('falha ao gravar'); }
$before = (int) filesize($path);
$after = (int) filesize($tmp);
if (!@rename($tmp, $path)) { // alguns FS não substituem no rename
@unlink($path);
if (!@rename($tmp, $path)) { @unlink($tmp); return $fail('falha ao substituir'); }
}
return ['status' => 'resized', 'saved' => max(0, $before - $after), 'reason' => "{$w}×{$h} → {$nw}×{$nh}"];
}
// Run img_optimize() over every raster file directly inside $dir (no recursion).
// Returns aggregate counters: resized / skipped / failed / saved (bytes).
function img_optimize_dir(string $dir, int $max_dim): array {
@set_time_limit(0);
$stats = ['resized' => 0, 'skipped' => 0, 'failed' => 0, 'saved' => 0];
if (!is_dir($dir)) return $stats;
foreach (scandir($dir) ?: [] as $file) {
$path = rtrim($dir, '/\\') . DIRECTORY_SEPARATOR . $file;
if (!is_file($path)) continue;
$ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));
if (!in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp'], true)) continue;
$r = img_optimize($path, $max_dim);
$stats[$r['status']]++;
$stats['saved'] += $r['saved'];
}
return $stats;
}
// PT-PT flash message for the bulk buttons.
function img_stats_message(array $s): string {
$parts = [];
if ($s['resized']) {
$msg = $s['resized'] . ' imagem(ns) redimensionada(s)';
if ($s['saved'] > 0) $msg .= ' — poupados ' . img_human_bytes($s['saved']);
$parts[] = $msg;
}
if ($s['skipped']) $parts[] = $s['skipped'] . ' já dentro do limite ou não aplicável';
if ($s['failed']) $parts[] = $s['failed'] . ' com falha';
return $parts ? 'Otimização concluída: ' . implode(', ', $parts) . '.' : 'Nenhuma imagem encontrada.';
}
function img_human_bytes(int $bytes): string {
if ($bytes >= 1048576) return number_format($bytes / 1048576, 1, ',', '') . ' MB';
if ($bytes >= 1024) return number_format($bytes / 1024, 0) . ' KB';
return $bytes . ' B';
}