PHP

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

Изображения нужно сжимать для ускорения скорости загрузки сайта, но как это сделать? На многих хостингах нет возможности устанавливать приложения, поэтому использование 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;
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));
            }
        }
    }    
}

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

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

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

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']));
    }
}
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>

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

26 октября 2018
В последнее время письма отправляемые с хостингов через функции mail() и mb_send_mail() часто попадают или совсем не...
В статье приведены основные примеры работы с расширением PHP PDO. Такие как подключение к БД, получение, изменение и...
Библиотека GD дает возможность работать с изображениями в PHP. Далее представлены примеры как изменить размер, вырезать...
В продолжении темы работы с массивами поговорим о типичной задаче – их сортировке. Для ее выполнения в PHP существует...