digital-garden/dev/snippet/Сжатие изображений без потери качества.md
Struchkov Mark 190ea59670
All checks were successful
continuous-integration/drone/push Build is passing
Сжатие изображений без потери качества.md
2024-09-25 15:15:45 +03:00

17 KiB
Raw Permalink Blame History

tags date zero-link parents linked
maturity/🌱
2023-11-20
../../meta/zero/00 Снипеты на bash
../fundamental/Сжатие данных

Данный скрипт предназначен для автоматического сжатия изображений форматов PNG и JPEG в заданной директории. Он использует современные утилиты сжатия, обеспечивая уменьшение размера файлов без заметной потери качества. Это особенно полезно для оптимизации веб-сайтов, ускорения загрузки страниц и экономии дискового пространства.

Note

Лучшее, на мой взгляд, приложение для сжатия jpg это JPEGmini Pro. Имеет версию cli для серверов. Но к сожалению оно платное. Его результат сжатия 2.2 мб —> 959 кб.

Установка необходимых утилит

Перед запуском скрипта необходимо установить следующие утилиты:

  • pngquant — инструмент для сжатия PNG-изображений.
  • zopflipng — утилита для дополнительной оптимизации PNG-файлов от Google.
  • mozjpeg — оптимизированный JPEG-кодек от Mozilla для сжатия JPEG-изображений.
# Для Debian/Ubuntu
sudo apt-get install pngquant
sudo apt-get install zopfli
sudo apt-get install mozjpeg

# Для macOS с использованием Homebrew
brew install pngquant
brew install zopfli
brew install mozjpeg
#!/bin/bash  
  
# Настройки  
IMAGE_DIR="./images"  
COMP_DIR="$IMAGE_DIR/comp"
# Автоматическое определение количества ядер процессора  
THREADS=$(getconf _NPROCESSORS_ONLN)  
echo "Используется $THREADS потоков для обработки."  
# Файлы логирования  
LOG_FILE="./zip_image_compression.log"  
ERROR_LOG_FILE="./zip_image_error.log"  
  
# Экспортируем необходимые переменные и функции для использования в subshell  
export IMAGE_DIR COMP_DIR LOG_FILE ERROR_LOG_FILE  
  
# Функция для определения размера файла  
get_file_size() {  
    local file="$1"  
    if [[ "$OSTYPE" == "linux-gnu"* ]]; then  
        stat -c%s "$file"  
    elif [[ "$OSTYPE" == "darwin"* ]]; then  
        stat -f%z "$file"  
    else  
        # Попытка использовать GNU stat, если установлен  
        if command -v gstat &> /dev/null; then  
            gstat -c%s "$file"  
        else  
            # В качестве альтернативы используем wc -c  
            wc -c < "$file" | tr -d ' '  
        fi  
    fi}  
  
export -f get_file_size  
  
# Функция для логирования успеха  
log_success() {  
    local message="$1"  
    echo "$message"  
    echo "$(date '+%Y-%m-%d %H:%M:%S') $message" >> "$LOG_FILE"  
}  
  
# Функция для логирования ошибок  
log_error() {  
    local message="$1"  
    echo "$message" >&2  
    echo "$(date '+%Y-%m-%d %H:%M:%S') $message" >> "$ERROR_LOG_FILE"  
}  
  
# Экспортируем функции логирования  
export -f log_success  
export -f log_error  
  
# Функция для обработки PNG файлов  
process_png() {  
    local input_file="$1"  
    local relative_path="${input_file#$IMAGE_DIR/}"  
    local output_file="$COMP_DIR/$relative_path"  
    local output_dir  
    output_dir="$(dirname "$output_file")"  
  
    mkdir -p "$output_dir"  
  
    # Проверка файла ошибки  
    local error_file="${output_file}.error"  
    if [ -f "$error_file" ]; then  
        # Файл ошибки существует, пропускаем обработку  
        return  
    fi  
  
    # Проверка хеша файла  
    local hash_file="${output_file}.md5"  
    local current_hash  
    current_hash="$(md5sum "$input_file" | awk '{print $1}')"  
  
    if [ -f "$hash_file" ]; then  
        local previous_hash  
        previous_hash="$(cat "$hash_file")"  
        if [ "$current_hash" == "$previous_hash" ]; then  
            # Файл не изменился, ничего не делаем  
            return  
        fi  
    fi  
    # Используем временный файл для обработки  
    local temp_output_file="${output_file}.tmp"  
    cp "$input_file" "$temp_output_file"  
  
    # Размер до сжатия  
    local original_size  
    original_size=$(get_file_size "$input_file")  
  
    # Используем pngquant  
    if ! pngquant --quality=90-100 --speed 1 --output "$temp_output_file" --force "$input_file"; then  
        local error_msg="Ошибка при сжатии $input_file с помощью pngquant"        log_error "$error_msg"  
        echo "$error_msg" > "$error_file"  
        rm -f "$temp_output_file"  
        return 1  
    fi  
  
    # Дополнительная оптимизация с помощью zopflipng  
    if ! zopflipng -y "$temp_output_file" "$temp_output_file"; then  
        local error_msg="Ошибка при оптимизации $temp_output_file с помощью zopflipng"        log_error "$error_msg"  
        echo "$error_msg" > "$error_file"  
        rm -f "$temp_output_file"  
        return 1  
    fi  
  
    # Размер после сжатия  
    local new_size  
    new_size=$(get_file_size "$temp_output_file")  
  
    # Проверка, что original_size не равен нулю  
    if [ "$original_size" -eq 0 ]; then  
        local error_msg="Ошибка: размер оригинального файла равен 0 для $input_file"  
        log_error "$error_msg"  
        echo "$error_msg" > "$error_file"  
        rm -f "$temp_output_file"  
        return 1  
    fi  
  
    # Проверка, уменьшился ли размер файла  
    if [ "$new_size" -ge "$original_size" ]; then  
        log_success "Сжатие не уменьшило размер файла $input_file, пропускаем сохранение"  
        # Сохраняем хеш, чтобы не обрабатывать файл снова  
        echo "$current_hash" > "$hash_file"  
        rm -f "$temp_output_file"  
        return  
    fi  
  
    # Процент сжатия  
    local reduction  
    reduction=$(awk "BEGIN {printf \"%.2f\", (($original_size - $new_size) / $original_size) * 100}")  
  
    log_success "Сжат PNG файл: $input_file на $reduction% ($original_size байт -> $new_size байт)"  
    # Перемещаем временный файл на место выходного  
    mv "$temp_output_file" "$output_file"  
  
    # Сохраняем хеш  
    echo "$current_hash" > "$hash_file"  
}  
export -f process_png  
  
# Функция для обработки JPEG файлов  
process_jpeg() {  
    local input_file="$1"  
    local relative_path="${input_file#$IMAGE_DIR/}"  
    local output_file="$COMP_DIR/$relative_path"  
    local output_dir  
    output_dir="$(dirname "$output_file")"  
  
    mkdir -p "$output_dir"  
  
    # Проверка файла ошибки  
    local error_file="${output_file}.error"  
    if [ -f "$error_file" ]; then  
        # Файл ошибки существует, пропускаем обработку  
        return  
    fi  
  
    # Проверка хеша файла  
    local hash_file="${output_file}.md5"  
    local current_hash  
    current_hash="$(md5sum "$input_file" | awk '{print $1}')"  
  
    if [ -f "$hash_file" ]; then  
        local previous_hash  
        previous_hash="$(cat "$hash_file")"  
        if [ "$current_hash" == "$previous_hash" ]; then  
            # Файл не изменился, ничего не делаем  
            return  
        fi  
    fi  
    # Используем временный файл для обработки  
    local temp_output_file="${output_file}.tmp"  
    cp "$input_file" "$temp_output_file"  
  
    # Размер до сжатия  
    local original_size  
    original_size=$(get_file_size "$input_file")  
  
    # Используем mozjpeg  
    if ! cjpeg -quality 95 -progressive -optimize -outfile "$temp_output_file" "$input_file"; then  
        local error_msg="Ошибка при сжатии $input_file с помощью mozjpeg"        log_error "$error_msg"  
        echo "$error_msg" > "$error_file"  
        rm -f "$temp_output_file"  
        return 1  
    fi  
  
    # Размер после сжатия  
    local new_size  
    new_size=$(get_file_size "$temp_output_file")  
  
    # Проверка, что original_size не равен нулю  
    if [ "$original_size" -eq 0 ]; then  
        local error_msg="Ошибка: размер оригинального файла равен 0 для $input_file"  
        log_error "$error_msg"  
        echo "$error_msg" > "$error_file"  
        rm -f "$temp_output_file"  
        return 1  
    fi  
  
    # Проверка, уменьшился ли размер файла  
    if [ "$new_size" -ge "$original_size" ]; then  
        log_success "Сжатие не уменьшило размер файла $input_file, пропускаем сохранение"  
        # Сохраняем хеш, чтобы не обрабатывать файл снова  
        echo "$current_hash" > "$hash_file"  
        rm -f "$temp_output_file"  
        return  
    fi  
  
    # Процент сжатия  
    local reduction  
    reduction=$(awk "BEGIN {printf \"%.2f\", (($original_size - $new_size) / $original_size) * 100}")  
  
    log_success "Сжат JPEG файл: $input_file на $reduction% ($original_size байт -> $new_size байт)"  
    # Перемещаем временный файл на место выходного  
    mv "$temp_output_file" "$output_file"  
  
    # Сохраняем хеш  
    echo "$current_hash" > "$hash_file"  
}  
export -f process_jpeg  
  
# Обработка PNG файлов  
find "$IMAGE_DIR" -type f \  
    -not -path "$COMP_DIR/*" \  
    ! -name "*-no-comp.*" \  
    -iname "*.png" -print0 | \  
xargs -0 -P "$THREADS" -I {} bash -c 'process_png "$@"' _ {}  
  
# Обработка JPEG файлов  
find "$IMAGE_DIR" -type f \  
    -not -path "$COMP_DIR/*" \   
    ! -name "*-no-comp.*" \  
    -iregex '.*\.\(jpg\|jpeg\)' -print0 | \  
xargs -0 -P "$THREADS" -I {} bash -c 'process_jpeg "$@"' _ {}

Описание работы скрипта

Основные функции скрипта

  • Автоматическое сжатие изображений: скрипт обрабатывает все PNG и JPEG файлы в заданной директории, за исключением тех, которые помечены для исключения.
  • Параллельная обработка: использует все доступные ядра процессора для ускорения процесса сжатия.
  • Избежание повторной обработки: проверяет хеши файлов, чтобы не сжимать уже обработанные или не изменившиеся файлы.
  • Логирование: записывает информацию об успешных операциях и ошибках в лог-файлы.

Детали работы

  • Настройка окружения:
    • Директории:
      • IMAGE_DIR: основная директория с изображениями (./images).
      • COMP_DIR: директория для сохранения сжатых изображений (./images/comp).
  • Потоки обработки: Скрипт автоматически определяет количество доступных процессорных ядер и использует их для параллельной обработки.
  • Логирование:
    • Файлы логов:
      • zip_image_compression.log: журнал успешных операций.
      • zip_image_error.log: журнал ошибок.
    • Функции логирования:
      • log_success: записывает успешные операции.
      • log_error: записывает ошибки.
  • Обработка изображений:
    • Проверка изменений:
      • Для каждого файла вычисляется MD5-хеш.
      • Если файл не изменился с момента последней обработки, он пропускается.
      • Если при предыдущей попытке была ошибка (существует файл с расширением .error), файл пропускается.
    • Сжатие PNG (process_png):
      • Использует pngquant для сжатия с качеством 90-100 и максимальной степенью сжатия.
      • Далее оптимизирует изображение с помощью zopflipng.
      • Сравнивает размер исходного и сжатого файлов; если размер не уменьшился, сжатый файл не сохраняется.
    • Сжатие JPEG (process_jpeg):
      • Использует mozjpeg (cjpeg) для сжатия с качеством 95 и прогрессивной разверткой.
      • Аналогично проверяет эффективность сжатия перед сохранением.
  • Параллельная обработка:
    • Использует команду find для поиска всех изображений требуемых форматов.
    • Пропускает файлы, содержащие -no-comp в названии, а также файлы в директории comp.
    • Команда xargs с параметром -P запускает обработку в несколько потоков.

Как использовать скрипт

  1. Подготовка:
    1. Поместите все изображения, которые необходимо сжать, в директорию ./images.
    2. Убедитесь, что установлены все необходимые утилиты.
  2. Запуск скрипта:
    1. Сделайте скрипт исполняемым: chmod +x your_script_name.sh
  3. Запустите скрипт: ./your_script_name.sh
  4. Результаты обработки:
    1. Сжатые изображения будут сохранены в директории ./images/comp, сохраняя структуру исходной директории.
    2. Логи успешных операций и ошибок можно просмотреть в файлах zip_image_compression.log и zip_image_error.log соответственно.

Пример результатов сжатия

!../../meta/files/images/Pasted image 20240925150631.png

Nginx

Теперь мы научим nginx при запросе изображений сначала пытаться отдать сжатое изображение, и если его не будет, то оригинал:

location ~* ^(/blog/ru/content/images/)(.+)\.(png|jpe?g)$ { 
	expires 1d;
	add_header Cache-Control "public, must-revalidate, proxy-revalidate";
	root /var/site/images;
	try_files /comp/$2.$3 $uri =404;
}

Мета информация

Область:: ../../meta/zero/00 Снипеты на bash Родитель:: ../fundamental/Сжатие данных Источник:: Автор:: Создана:: 2023-11-20

Дополнительные материалы

Дочерние заметки