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

18 KiB
Raw Permalink Blame History

aliases tags date zero-link parents linked
maturity/🌱
2024-09-05
../../meta/zero/00 Снипеты на bash

PNG и JPG являются хорошими форматами изображений, которые можно сжать без потери качества. Однако, на сегодняшний день существует более современный формат WebP, который может показать еще более эффективные результаты при сжатии изображений, но с едва заметным ухудшением качества.

В документации WebP перечислено множество параметров, которые повлияют на качество получаемого изображения. Вы можете провести экспериментальный подбор параметров онлайн на сайте https://squoosh.app.

Я вы выбрал для себя следующий набор параметров:

cwebp -mt -af -progress -m 6 -q 80 -pass 10 input.jpg -o output.webp

Это команда преобразует JPG файл input.jpg в файл изображения WebP с именем output.webp. Команда включает несколько опций, которые управляют процессом кодирования:

  • -mt включает многопоточность, что может улучшить производительность на многоядерных процессорах.
  • -af включает автоматическую фильтрацию, которая применяет алгоритм фильтрации для повышения эффективности сжатия.
  • -progress выводит показывает процент обработки файла.
  • -m 6 устанавливает максимальное количество сегментов, используемых в процессе кодирования, равным 6, что может повысить эффективность сжатия за счет увеличения времени кодирования.
  • -q 80 устанавливает коэффициент качества на 80, который контролирует степень сжатия и влияет на визуальное качество выходного изображения.
  • -pass 10 устанавливает число проходов кодирования равным 10, что может повысить эффективность сжатия за счет увеличения времени кодирования.

Warning

-lossless позволяет использовать сжатие без потерь. Но тогда ваше новое изображение может оказаться существенно тяжелее исходного.

Улучшим скрипт сжатия изображений и добавим также преобразование в webp:

#!/bin/bash  
  
# Настройки  
IMAGE_DIR="./images"  
COMP_DIR="$IMAGE_DIR/comp"  
WEBP_DIR="$IMAGE_DIR/webp"  
# Автоматическое определение количества ядер процессора  
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 WEBP_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  
  
# Функция для конвертации в WebP  
process_webp() {  
    local input_file="$1"  
    local relative_path="${input_file#$IMAGE_DIR/}"  
    local output_file="$WEBP_DIR/$relative_path"  
    output_file="${output_file%.*}.webp"  
    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 original_size  
    original_size=$(get_file_size "$input_file")  
  
    # Создаем временный файл для вывода  
    local temp_output_file="${output_file}.tmp"  
  
    if ! cwebp -mt -af -quiet -m 6 -q 95 -pass 10 "$input_file" -o "$temp_output_file"; then  
        local error_msg="Ошибка при конвертации $input_file в WebP"        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 "Конвертация в WebP не уменьшила размер файла $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 "Конвертирован в WebP: $input_file на $reduction% ($original_size байт -> $new_size байт)"  
    # Перемещаем временный файл на место выходного  
    mv "$temp_output_file" "$output_file"  
  
    # Сохраняем хеш  
    echo "$current_hash" > "$hash_file"  
}  
export -f process_webp  
  
# Обработка PNG файлов  
find "$IMAGE_DIR" -type f \  
    -not -path "$COMP_DIR/*" \  
    -not -path "$WEBP_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/*" \  
    -not -path "$WEBP_DIR/*" \  
    ! -name "*-no-comp.*" \  
    -iregex '.*\.\(jpg\|jpeg\)' -print0 | \  
xargs -0 -P "$THREADS" -I {} bash -c 'process_jpeg "$@"' _ {}  
  
# Конвертация в WebP из исходных файлов  
find "$IMAGE_DIR" -type f \( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' \) \  
    -not -path "$COMP_DIR/*" \  
    -not -path "$WEBP_DIR/*" \  
    ! -name "*-no-comp.*" \  
    -print0 | \  
xargs -0 -P "$THREADS" -I {} bash -c 'process_webp "$@"' _ {}

Мы берем сжатые изображения из папки comp и преобразуем их в WebP, складывая в отдельную папку webp. Если вы захотите использовать другие параметры сжатия, вы всегда сможете пересоздать изображения с новыми параметрами.

Тесты преобразования

Сожмем наши файл размером в 2,7 мб и 2.2 в формат WebP с разными параметрами качества:

  • -q 90: Размер 325 кб.
  • -q 85: Размер 267 кб.
  • -q 80: Размер 229 кб.
  • -q 75: Размер 200 кб.
  • -q 70: Размер 191 кб.
  • -q 60: Размер 176 кб.
  • -q 50: Размер 163 кб.
  • -q 40: Размер 147 кб.
  • -q 30: Размер 129 кб.
  • -q 20: Размер 115 кб.
  • -q 10: Размер 90 кб.
  • -q 1: Размер 70 кб. Для экстренных случаев. Например, вы заблудились в лесу и надо отправить фото по спутниковой сети 😅

Пример использования на реальных изображениях моего блога !../../meta/files/images/Pasted image 20240925144640.png

Nginx

Теперь мы научим nginx при запросе изображений сначала пытаться найти WebP файл, и только потом отдавать сжатый PNG/JPG, а если и сжатого нет, то отдавать обычный файл.

Для этого напишем следующий location:

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 /webp/$2.webp /comp/$2.$3 $uri =404;
}

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

Область:: ../../meta/zero/00 Снипеты на bash Родитель:: Источник:: Автор:: Создана:: 2024-09-05

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

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