Изображения нужно сжимать для ускорения скорости загрузки сайта, но как это сделать? На многих хостингах нет возможности устанавливать приложения, поэтому использование unix приложений optipng, pngcrush, jpegtran отпадает. Выкачивать картинки и сжимать их FileOptimizer или другими программами не продуктивно т.к. через время всю процедуру нужно повторять.
Решение – использовать сервисы для сжатия изображений по API, например tinypng.com. Поддерживает PNG и JPG до 5mb, бесплатен до 500 фото в месяц.
Напишем скрипт который по крону будет автоматом собирать файлы и отправлять их на сжатие.
Создадим таблицу `optimize_img`
, в которой будут храниться пути и имена файлов, статус обработки и ошибки. Поле `diff`
используется для хранения разницы между оригинальным и сжатым файлом для статистики.
CREATE TABLE IF NOT EXISTS `optimize_img` (
`id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`img` varchar(255) NOT NULL,
`done` tinyint(1) UNSIGNED NOT NULL DEFAULT '0',
`error` varchar(255) NOT NULL,
`diff` int(11) UNSIGNED NOT NULL DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
Найдем все картинки в нужных категориях сайта с помощью рекурсивной версии функции glob()
и добавим новые в БД.
// Подключение к БД.
$dbh = new PDO('mysql:dbname=db_name;host=localhost', 'логин', 'пароль');
// Категории в которых лежат картинки.
$dirs = array(
__DIR__ . '/themes',
__DIR__ . '/uploads',
);
// Расширения файлов.
$exts = array('png', 'jpg', 'jpeg');
function glob_recursive($pattern, $flags = 0)
{
$files = glob($pattern, $flags);
foreach (glob(dirname($pattern) . '/*', GLOB_ONLYDIR|GLOB_NOSORT) as $dir) {
$files = array_merge($files, glob_recursive($dir . '/' . basename($pattern), $flags));
}
return $files;
}
foreach ($dirs as $dir) {
foreach (glob_recursive($dir . '/*.*') as $file) {
$ext = strtolower(substr(strrchr($file, '.'), 1));
if (in_array($ext, $exts)) {
$file = str_replace(__DIR__, '', $file);
$sth = $dbh->prepare("SELECT * FROM `optimize_img` WHERE `img` = ?");
$sth->execute(array($file));
$isdb = $sth->fetch(PDO::FETCH_ASSOC);
if (empty($isdb)) {
$sth = $dbh->prepare("INSERT INTO `optimize_img` SET `img` = ?");
$sth->execute(array($file));
}
}
}
}
Таблица заполнится данными:
Получим ключ к API, для этого нужно зарегистрироваться на странице tinypng.com/developers.
После отправки формы придет письмо с ссылкой в личный кабинет.
Получим запись из БД и отправим POST-запрос в формате JSON:
// Ключ API tinypng.com
$api_key = 'xxxxxxxxxxxxxxxxxxxxxxxxxxx';
$ch = curl_init('https://api.tinify.com/shrink');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_USERPWD, ':' . $api_key);
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json'));
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS,
json_encode(
array(
'source' => array(
'url' => 'http://example.com' . str_replace('%2F', '/', rawurlencode($item['img']))
)
)
)
);
$res = curl_exec($ch);
curl_close($ch);
$res = json_decode($res, JSON_OBJECT_AS_ARRAY);
var_dump($res);
URL-кодирование str_replace('%2F', '/', rawurlencode($item['img']))
нужно для файлов с русскими названиями и пробелами иначе возникают ошибки.
Сервис возвращает данные в JSON:
Array(
[input] => Array(
[size] => 26109
[type] => image/png
)
[output] => Array(
[size] => 11465
[type] => image/png
[width] => 155
[height] => 130
[ratio] => 0.4391
[url] => https://api.tinify.com/output/90uwbe47cg741bh68r805wzapwp9brfh
)
)
Обработаем ответ. Если все прошло без ошибок, исходный файл переименуется в .bak, на его место с копируется новый.
if (!empty($res['error'])) {
// Сервис вернул ошибку.
$sth = $dbh->prepare("UPDATE `optimize_img` SET `error` = ? WHERE `id` = ?");
$sth->execute(array($res['message'], $item['id']));
} elseif (!empty($res['output']['url'])) {
// Файл сжат, пробуем заменить им исходный.
$file = __DIR__ . $item['img'];
if (rename($file, $file . '.bak')) {
if (copy($res['output']['url'], $file)) {
$diff = $res['input']['size'] - $res['output']['size'];
$sth = $dbh->prepare("UPDATE `optimize_img` SET `done` = 1, `diff` = ? WHERE `id` = ?");
$sth->execute(array($diff, $item['id']));
exit;
} else {
rename($file . '.bak', $file);
}
}
$sth = $dbh->prepare("UPDATE `optimize_img` SET `error` = ? WHERE `id` = ?");
$sth->execute(array('Ошибка замены файла', $item['id']));
}
Соберем и оптимизируем весь код в один файл optimize_img.php
При каждом запуске скрипта идет проверка на наличие несжатых файлов в БД, если очередь закончилась, то идет поиск и добавление новых файлов, и так по кругу.
<?php
// Подключение к БД.
$dbh = new PDO('mysql:dbname=db_name;host=localhost', 'логин', 'пароль');
// Категории в которых лежат картинки.
$dirs = array(
__DIR__ . '/themes',
__DIR__ . '/uploads',
);
// Расширения файлов.
$exts = array('png', 'jpg', 'jpeg');
// Ключ API tinypng.com
$api_key = 'xxxxxxxxxxxxxxxxxxxxxxxxxxx';
function glob_recursive($pattern, $flags = 0)
{
$files = glob($pattern, $flags);
foreach (glob(dirname($pattern) . '/*', GLOB_ONLYDIR|GLOB_NOSORT) as $dir) {
$files = array_merge($files, glob_recursive($dir . '/' . basename($pattern), $flags));
}
return $files;
}
// Получим файл из очереди.
$sth = $dbh->prepare("SELECT * FROM `optimize_img` WHERE `done` = 0 AND `error` = '' ORDER BY `id` DESC");
$sth->execute();
$item = $sth->fetch(PDO::FETCH_ASSOC);
if (empty($item)) {
// Если больше нет файлов, соберем и добавим новые.
foreach ($dirs as $dir) {
foreach (glob_recursive($dir . '/*.*') as $file) {
$ext = strtolower(substr(strrchr($file, '.'), 1));
if (in_array($ext, $exts)) {
$file = str_replace(__DIR__, '', $file);
$sth = $dbh->prepare("SELECT * FROM `optimize_img` WHERE `img` = ?");
$sth->execute(array($file));
$isdb = $sth->fetch(PDO::FETCH_ASSOC);
if (empty($isdb)) {
$sth = $dbh->prepare("INSERT INTO `optimize_img` SET `img` = ?");
$sth->execute(array($file));
}
}
}
}
} elseif (!is_file(__DIR__ . $item['img'])) {
// Если файл уже удалили.
$sth = $dbh->prepare("DELETE FROM `optimize_img` WHERE `id` = ?");
$sth->execute(array($item['id']));
} else {
// Отправка файла в tinypng.com
$ch = curl_init('https://api.tinify.com/shrink');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_USERPWD, ':' . $api_key);
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json'));
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS,
json_encode(
array(
'source' => array(
'url' => 'http://example.com' . str_replace('%2F', '/', rawurlencode($item['img']))
)
)
)
);
$res = curl_exec($ch);
curl_close($ch);
$res = json_decode($res, JSON_OBJECT_AS_ARRAY);
//var_dump($res);
if (!empty($res['error'])) {
// Сервис вернул ошибку.
$sth = $dbh->prepare("UPDATE `optimize_img` SET `error` = ? WHERE `id` = ?");
$sth->execute(array($res['message'], $item['id']));
} elseif (!empty($res['output']['url'])) {
// Файл сжат, пробуем заменить им исходный.
$file = __DIR__ . $item['img'];
if (rename($file, $file . '.bak')) {
if (copy($res['output']['url'], $file)) {
$diff = $res['input']['size'] - $res['output']['size'];
$sth = $dbh->prepare("UPDATE `optimize_img` SET `done` = 1, `diff` = ? WHERE `id` = ?");
$sth->execute(array($diff, $item['id']));
exit;
} else {
rename($file . '.bak', $file);
}
}
$sth = $dbh->prepare("UPDATE `optimize_img` SET `error` = ? WHERE `id` = ?");
$sth->execute(array('Ошибка замены файла', $item['id']));
}
}
Чтобы не превысить лимит 500 шт в месяц, скрипт должен запускаться:
31 день * 24 часа = 744 часов / 500 ≈ 1 раз в 1,5 часа, округлим до интервала в 2 часа.
Запуск скрипта в cron по URL выполняется следующий командой:
/usr/local/bin/wget -O - -q "http://example.com/optimize_img.php"
Временной интервал (минута, час, день, месяц, день недели):
0 */2 * * *
Мастерхост
Timeweb
RU-CENTER
Чтобы отслеживать состояние процесса оптимизации, можно использовать следующий скрипт.
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">
<title>Отчет оптимизации изображений</title>
</head>
<body>
<div class="container" style="max-width: 900px;">
<h2>Отчет оптимизации изображений</h2>
<dl class="row">
<dt class="col-sm-3">Всего:</dt>
<dd class="col-sm-9">
<?php
$dbh = new PDO('mysql:dbname=db_name;host=localhost', 'логин', 'пароль');
$sth = $dbh->prepare("SELECT COUNT(`id`) FROM `optimize_img`");
$sth->execute();
echo $sth->fetch(PDO::FETCH_COLUMN);
?> шт
</dd>
<dt class="col-sm-3">Завершено:</dt>
<dd class="col-sm-9">
<?php
$sth = $dbh->prepare("SELECT COUNT(`id`) FROM `optimize_img` WHERE `done` = 1");
$sth->execute();
echo $sth->fetch(PDO::FETCH_COLUMN);
?> шт
</dd>
<dt class="col-sm-3">Оптимизировано:</dt>
<dd class="col-sm-9">
<?php
$sth = $dbh->prepare("SELECT SUM(`diff`) FROM `optimize_img` WHERE `done` = 1");
$sth->execute();
echo round($sth->fetch(PDO::FETCH_COLUMN) / 1024 / 1024, 2);
?> МБ
</dd>
<dt class="col-sm-3">Ошибки:</dt>
<dd class="col-sm-9">
<?php
$sth = $dbh->prepare("SELECT COUNT(`id`) FROM `optimize_img` WHERE `error` <> ''");
$sth->execute();
echo $sth->fetch(PDO::FETCH_COLUMN);
?> шт
</dd>
</dl>
<?php
$sth = $dbh->prepare("SELECT * FROM `optimize_img` WHERE `error` <> ''");
$sth->execute();
$errors = $sth->fetchAll(PDO::FETCH_ASSOC);
if (!empty($errors)) {
foreach ($errors as $error) {
?>
<div class="alert alert-danger" role="alert">
<strong><?php echo $error['img']; ?></strong>
<br><?php echo $error['error']; ?>
</div>
<?php
}
}
?>
</div>
</body>
</html>
1) `error` varchar(255) NOT NULL, => `error` varchar(255) NULL,
Иначе при выполнении запроса "INSERT INTO `optimize_img` SET `img` = ?" выкидывало ошибку: отсутсвует значение по умолчанию для `error`.
Соответственно запрос:
// Получим файл из очереди.
$sth = $dbh->prepare("SELECT * FROM `optimize_img` WHERE `done` = 0 AND `error` IS NULL ORDER BY `id` DESC");
2) Отказывалась работать copy:
if (copy($res['output']['url'], $file)) {
...
}
заменил на:
if (rename($file, $file . '.bak')) {
$ch = curl_init($res['output']['url']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$output = curl_exec($ch);
$fh = fopen($file, 'w');
if (fwrite($fh, $output) !== FALSE) {
fclose($fh);
$diff = $res['input']['size'] - $res['output']['size'];
$sth = $dbh->prepare("UPDATE `optimize_img` SET `done` = 1, `diff` = ? WHERE `id` = ?");
$sth->execute(array($diff, $item['id']));
exit;
} else {
rename($file . '.bak', $file);
}
}