Автоматическое сжатие и оптимизация картинок на сайте

Изображения нужно сжимать для ускорения скорости загрузки сайта, но как это сделать? На многих хостингах нет возможности устанавливать приложения, поэтому использование unix приложений optipng, pngcrush, jpegtran отпадает. Выкачивать картинки и сжимать их FileOptimizer или другими программами не продуктивно т.к. через время всю процедуру нужно повторять.

Решение – использовать сервисы для сжатия изображений по API, например tinypng.com. Поддерживает PNG и JPG до 5mb, бесплатен до 500 фото в месяц.

Напишем скрипт который по крону будет автоматом собирать файлы и отправлять их на сжатие.

1

База данных

Создадим таблицу `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;
SQL
2

Поиск файлов на сервере

Найдем все картинки в нужных категориях сайта с помощью рекурсивной версии функции 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));
			}
		}
	}	
}
PHP

Таблица заполнится данными:

Таблица БД заполнится данными

3

Отправка файлов на сжатие

Получим ключ к API, для этого нужно зарегистрироваться на странице tinypng.com/developers.

tinypng.com регистрация

После отправки формы придет письмо с ссылкой в личный кабинет.

tinypng.com ключ API

Получим запись из БД и отправим 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);
PHP

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
	)
)
JS

Обработаем ответ. Если все прошло без ошибок, исходный файл переименуется в .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']));
}
PHP

Соберем и оптимизируем весь код в один файл 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']));
	}
}
PHP
4

Запуск по CRON

Чтобы не превысить лимит 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 * * *

Мастерхост

Настройка Cron в Мастерхост

Timeweb

Настройка Cron в Timeweb

RU-CENTER

Настройка Cron в nic.ru

5

Отчет о процессе

Чтобы отслеживать состояние процесса оптимизации, можно использовать следующий скрипт.
<!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>
HTML

Отчет оптимизации изображений

26.10.2018, обновлено 11.09.2019 1798
Предыдущая запись Изображения WebP в PHP
Следующая запись Обрезка текста для анонса

Поделится

Другие публикации

cURL PHP – это библиотека предназначенная для получения и передачи данных через такие протоколы, как HTTP, FTP, HTTPS....
После регистрации в системе эквайринга Сбербанка и получив доступ к тестовой среде, можно приступить к интеграции с...
В статье рассмотрены примеры как вывести метку на карту из БД и вывод других объектов, которые находятся рядом.
Для начала вы должны быть авторизированы в VK и являться администратором группы или страницы. Далее нужно создать...
Рассмотрим пример как найти в базе данных соседние объекты по координатам и вывести их на карте Яндекс.
В статье приведены основные примеры работы с расширением PHP PDO. Такие как подключение к БД, получение, изменение и...