diff --git a/.quartz/zip_image.sh b/.quartz/zip_image.sh deleted file mode 100644 index a1dec22c..00000000 --- a/.quartz/zip_image.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash - -find ./images/ -type f -not -path "./images/comp/*" ! -name "*-no-comp.*" $option -iname "*.png" -exec sh -c ' - png_file="${1/\/images\//\/images\/comp\/}" - png_dir="$(dirname "$png_file")" - mkdir -p "$png_dir" - cp "$1" "${png_file}" - optipng -o7 "${png_file}" - advpng -z4 "${png_file}" - pngcrush -rem gAMA -rem alla -rem cHRM -rem iCCP -rem sRGB -rem time -ow "${png_file}" -' _ {} \; - -find ./images/ -type f-not -path "./images/comp/*" ! -name "*-no-comp.*" $option -iregex '.*\.\(jpg\|jpeg\)' -exec sh -c ' - jpg_file="${1/\/images\//\/images\/comp\/}" - jpg_dir="$(dirname "$jpg_file")" - mkdir -p "$jpg_dir" - cp "$1" "${jpg_file}" - jpegoptim --all-progressive "${jpg_file}" -' _ {} \; - -find ./images/comp -type f -iregex '.*\.\(jpg\|jpeg\|png\)' -not -iregex '.*no-comp\.\(jpg\|jpeg\|png\)' $option -exec sh -c ' - webp_file="${1/\/images\/comp\//\/images\/webp\/}" - webp_dir="$(dirname "$webp_file")" - mkdir -p "$webp_dir" - cwebp -mt -af -progress -m 6 -q 75 -pass 10 "$1" -o "${webp_file%.*}.webp" -' _ {} \; \ No newline at end of file diff --git a/dev/snippet/Преобразование изображений в Webp.md b/dev/snippet/Преобразование изображений в Webp.md index 32e4af8f..ee4c9c28 100644 --- a/dev/snippet/Преобразование изображений в Webp.md +++ b/dev/snippet/Преобразование изображений в Webp.md @@ -30,20 +30,340 @@ cwebp -mt -af -progress -m 6 -q 80 -pass 10 input.jpg -o output.webp > [!WARNING] > `-lossless` позволяет использовать сжатие без потерь. Но тогда ваше новое изображение может оказаться существенно тяжелее исходного. -Улучшим [скрипт сжатия изображений](Сжатие%20изображений%20без%20потери%20качества.md) и добавим преобразование в webp: +Улучшим [скрипт сжатия изображений](Сжатие%20изображений%20без%20потери%20качества.md) и добавим также преобразование в webp: -```bash -find ./images/comp -type f -iregex '.*\.\(jpg\|jpeg\|png\)' -not -iregex '.*no-comp\.\(jpg\|jpeg\|png\)' $option -exec sh -c ' - webp_file="${1/\/images\/comp\//\/images\/webp\/}" - webp_dir="$(dirname "$webp_file")" - mkdir -p "$webp_dir" - cwebp -mt -af -progress -m 6 -q 75 -pass 10 "$1" -o "${webp_file%.*}.webp" -' _ {} \; +```bash title="zip_image.sh" {6, 15} +#!/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 с разными параметрами качества: +Сожмем наши файл размером в 2,7 мб и 2.2 в формат WebP с разными параметрами качества: - -q 90: Размер 325 кб. - -q 85: Размер 267 кб. - -q 80: Размер 229 кб. @@ -56,27 +376,22 @@ find ./images/comp -type f -iregex '.*\.\(jpg\|jpeg\|png\)' -not -iregex '.*no-c - -q 20: Размер 115 кб. - -q 10: Размер 90 кб. - -q 1: Размер 70 кб. Для экстренных случаев. Например, вы заблудились в лесу и надо отправить фото по спутниковой сети 😅 + +Пример использования на реальных изображениях моего блога +![[../../meta/files/images/Pasted image 20240925144640.png]] ## Nginx Теперь мы научим nginx при запросе изображений сначала пытаться найти WebP файл, и только потом отдавать сжатый PNG/JPG, а если и сжатого нет, то отдавать обычный файл. Для этого напишем следующий `location`: ```nginx -location ~* ^(/blog/ru/content/images/)(.+)\.(png|jpe?g)$ { - expires max; - alias /var/struchkov.dev/ghost/www/images; - set $webp_image_subdir "/webp/"; - set $basename $2; - try_files $webp_image_subdir$basename$webp_suffix $uri; +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; } ``` - -Данный `location` обрабатывает все запросы к адресам, которые начинаются с `/images/`, за которым следует любое количество символов, затем точка, а затем формат файла png/jpeg/jpg. - -Для запросов, соответствующих этому шаблону, выполняются следующие действия: -- Директива `alias` указывает путь к локальной директории, из которой будут отдаваться файлы. В данном случае путь к директории `/images`. -- Директивы `set` назначают переменные, которые будут использоваться в последующих директивах. Переменная `$webp_image_subdir` устанавливается в `/webp/`, а переменная `$basename` устанавливается в захваченную подстроку шаблона регулярного выражения (т.е. имя файла без расширения). -- Директива `try_files` пытается отдать Webp-версию запрошенного файла изображения, добавляя к переменным `$webp_image_subdir` и `$basename` суффикс `.webp`. Если NGINX сможет найти Webp-версию запрашиваемого файла изображения, он отдаст его. Если он не может найти WebP-версию, то отдаст сжатый JPG/PNG, если и сжатого файла не будет, то отдаст не сжатое. *** ## Мета информация **Область**:: [[../../meta/zero/00 Снипеты на bash|00 Снипеты на bash]] diff --git a/dev/snippet/Сжатие изображений без потери качества.md b/dev/snippet/Сжатие изображений без потери качества.md index 9baccb6f..d613a8d7 100644 --- a/dev/snippet/Сжатие изображений без потери качества.md +++ b/dev/snippet/Сжатие изображений без потери качества.md @@ -5,109 +5,330 @@ date: 2023-11-20 zero-link: - "[[../../meta/zero/00 Снипеты на bash|00 Снипеты на bash]]" parents: - - "[[../garden/ru/dev/fundamental/Сжатие данных|Сжатие данных]]" + - "[[../fundamental/Сжатие данных|Сжатие данных]]" linked: --- -Размер изображений составляет существенную часть от размера страницы сайта. Поэтому часто я сжимаю изображения на своих сайтах. В этой заметке рассказываю какими способами я это делаю. - -```shell -#!/bin/bash -file=comp.flag - -if [ -f "$file" ]; then - option="-newer $file" -fi - -find ./images/ -type f -not -path "./images/comp/*" ! -name "*-no-comp.*" $option -iname "*.png" -exec sh -c ' - png_file="${1/\/images\//\/images\/comp\/}" - png_dir="$(dirname "$png_file")" - mkdir -p "$png_dir" - cp "$1" "${png_file}" - optipng -o7 "${png_file}" - advpng -z4 "${png_file}" - pngcrush -rem gAMA -rem alla -rem cHRM -rem iCCP -rem sRGB -rem time -ow "${png_file}" -' _ {} \; - -find ./images/ -type f-not -path "./images/comp/*" ! -name "*-no-comp.*" $option -iregex '.*\.\(jpg\|jpeg\)' -exec sh -c ' - jpg_file="${1/\/images\//\/images\/comp\/}" - jpg_dir="$(dirname "$jpg_file")" - mkdir -p "$jpg_dir" - cp "$1" "${jpg_file}" - jpegoptim --all-progressive "${jpg_file}" -' _ {} \; - -touch $file -echo "$(date)" > $file -``` - -Этот скрипт сжимает изображения без потери качества. Он размещается рядом с каталогом `images`, в котором находятся ваши изображения. Разберемся, как он работает. - -Сначала скрипт проверяет, существует ли в текущем каталоге файл `comp.flag`. Если файл существует, он устанавливает значение `-newer $file` в переменную `option`, которая будет использоваться в качестве фильтра для поиска только тех файлов, которые были изменены после даты создания файла `optimg.flag`. Если файл не существует, переменная `option` будет пустой. - -Затем скрипт использует команду `find` для рекурсивного поиска файлов в каталоге `images` и его подкаталогах, которые: -- `-type f`. Являются обычными файлами. -- Имеют расширение имени файла `.png`, `.jpeg` или `.jpg`. -- `-not -path`_._ Не находятся в указанных каталогах_._ В данном случае это каталоги `./images/comp`. Мы будем складывать туда сжатые изображения, и мы не хотим заново по ним проходить поиском и сжимать снова. -- `! -name "-no-comp."`. Оставим возможность не сжимать изображение, если его имя заканчивается на `-no-comp`. -- Для каждого из этих файлов сценарий использует свою команду оптимизации. - -Мы не будем затирать оригиналы изображений. Вместо этого мы создадим дополнительную папку `comp` в каталоге `images`, в которую и сложим преобразованные изображения. Создаваемая структура подкаталогов в `comp` будет повторять структуру подкатологов в `images`. - -Для файлов PNG сначала используется `optipng` для сжатия с самым высоким уровнем оптимизации (`-o7`). Далее используем `advpng` для дальнейшего сжатия с уровнем сжатия 4 (`-z4`). И наконец `pngcrush` для удаления из файла определенных фрагментов, которые можно безопасно удалить для уменьшения размера файла. - -Для файлов JPEG используется `jpegoptim` для их оптимизации со сжатием без потерь (`--all-progressive --strip-all`). - -После оптимизации всех подходящих изображений сценарий пересоздает файл `comp.flag`. Это гарантирует, что скрипт будет оптимизировать только те файлы, которые были изменены с момента последнего запуска. -## Тесты на сжатие -Возьмем два одинаковых изображения (3456  x  2234): одно форматом jpg и размером 2.2 мб, второе форматом png и размером 2,7 мб. - -Запускаем скрипт и смотрим на результат сжатия. - -```bash -** Processing: ./images/comp/image-two.png -3456x2234 pixels, 4x8 bits/pixel, RGB+alpha -Input IDAT size = 2664231 bytes -Input file size = 2666952 bytes - -Trying: - zc = 9 zm = 9 zs = 0 f = 0 IDAT size = 2374591 - zc = 9 zm = 8 zs = 0 f = 0 IDAT size = 2368803 - zc = 9 zm = 9 zs = 0 f = 1 IDAT size = 2142264 - zc = 9 zm = 8 zs = 0 f = 1 IDAT size = 2137390 - zc = 9 zm = 9 zs = 1 f = 1 IDAT size = 2134232 - zc = 9 zm = 8 zs = 1 f = 1 IDAT size = 2128765 - zc = 9 zm = 9 zs = 0 f = 4 IDAT size = 2074935 - zc = 9 zm = 8 zs = 0 f = 4 IDAT size = 2071117 - zc = 9 zm = 9 zs = 1 f = 4 IDAT size = 2055799 - zc = 9 zm = 8 zs = 1 f = 4 IDAT size = 2054390 - zc = 9 zm = 9 zs = 0 f = 5 IDAT size = 2046026 - zc = 9 zm = 8 zs = 0 f = 5 IDAT size = 2040193 - zc = 9 zm = 9 zs = 1 f = 5 IDAT size = 2031130 - zc = 9 zm = 8 zs = 1 f = 5 IDAT size = 2024568 - -Selecting parameters: - zc = 9 zm = 8 zs = 1 f = 5 IDAT size = 2024568 - -Output IDAT size = 2024568 bytes (639663 bytes decrease) -Output file size = 2025345 bytes (641607 bytes = 24.06% decrease) - - 2025345 2025345 100% ./images/comp/image-two.png (Bigger 2208631) - 2025345 2025345 100% -Warning: versions are different between png.h and png.c - png.h version: 1.6.34 - png.c version: 1.6.37 - - Recompressing IDAT chunks in ./images/comp/image-two.png - Total length of data found in critical chunks = 2024625 - Best pngcrush method = 10 (ws 15 fm 6 zl 9 zs 1) = 2055326 -CPU time decode 1.074758, encode 16.759768, other 0.018592, total 17.899076 sec -./images/comp/image-one.jpg 3456x2234 24bit N Exif XMP JFIF [OK] 2158567 --> 1947377 bytes (9.78%), optimized. -``` - -В результате мы смогли сжать png до 2.1 мб, а jpg до 1.9 мб. При этом сохранив исходный размер изображения и без потерь качества. +Данный скрипт предназначен для автоматического сжатия изображений форматов PNG и JPEG в заданной директории. Он использует современные утилиты сжатия, обеспечивая уменьшение размера файлов без заметной потери качества. Это особенно полезно для оптимизации веб-сайтов, ускорения загрузки страниц и экономии дискового пространства. > [!NOTE] > Лучшее, на мой взгляд, приложение для сжатия jpg это JPEGmini Pro. Имеет версию cli для серверов. Но к сожалению оно платное. Его результат сжатия 2.2 мб —> 959 кб. +## Установка необходимых утилит +Перед запуском скрипта необходимо установить следующие утилиты: +- **pngquant** — инструмент для сжатия PNG-изображений. +- **zopflipng** — утилита для дополнительной оптимизации PNG-файлов от Google. +- **mozjpeg** — оптимизированный JPEG-кодек от Mozilla для сжатия JPEG-изображений. + +```shell +# Для 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 +``` +- [[../linux/rhel/Установка утилиты mozjpeg на RHEL|Установка утилиты mozjpeg на RHEL]] + + +```shell title="zip_image.sh" +#!/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 при запросе изображений сначала пытаться отдать сжатое изображение, и если его не будет, то оригинал: + +```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; +} +``` *** diff --git a/meta/files/images/Pasted image 20240925144640.png b/meta/files/images/Pasted image 20240925144640.png new file mode 100644 index 00000000..f10bd09b Binary files /dev/null and b/meta/files/images/Pasted image 20240925144640.png differ diff --git a/meta/files/images/Pasted image 20240925150631.png b/meta/files/images/Pasted image 20240925150631.png new file mode 100644 index 00000000..2bb72ccb Binary files /dev/null and b/meta/files/images/Pasted image 20240925150631.png differ diff --git a/meta/files/images/comp/Pasted image 20240925144640.png b/meta/files/images/comp/Pasted image 20240925144640.png new file mode 100644 index 00000000..312c958a Binary files /dev/null and b/meta/files/images/comp/Pasted image 20240925144640.png differ diff --git a/meta/files/images/comp/Pasted image 20240925144640.png.md5 b/meta/files/images/comp/Pasted image 20240925144640.png.md5 new file mode 100644 index 00000000..ff44229e --- /dev/null +++ b/meta/files/images/comp/Pasted image 20240925144640.png.md5 @@ -0,0 +1 @@ +26c46c942b83a86396577c667e1c76ff diff --git a/meta/files/images/comp/Pasted image 20240925150631.png b/meta/files/images/comp/Pasted image 20240925150631.png new file mode 100644 index 00000000..61f98524 Binary files /dev/null and b/meta/files/images/comp/Pasted image 20240925150631.png differ diff --git a/meta/files/images/comp/Pasted image 20240925150631.png.md5 b/meta/files/images/comp/Pasted image 20240925150631.png.md5 new file mode 100644 index 00000000..092f600a --- /dev/null +++ b/meta/files/images/comp/Pasted image 20240925150631.png.md5 @@ -0,0 +1 @@ +d7022072fd2eda793f69449c31de699a diff --git a/meta/files/images/webp/Pasted image 20240925144640.webp b/meta/files/images/webp/Pasted image 20240925144640.webp new file mode 100644 index 00000000..02f14c35 Binary files /dev/null and b/meta/files/images/webp/Pasted image 20240925144640.webp differ diff --git a/meta/files/images/webp/Pasted image 20240925144640.webp.md5 b/meta/files/images/webp/Pasted image 20240925144640.webp.md5 new file mode 100644 index 00000000..ff44229e --- /dev/null +++ b/meta/files/images/webp/Pasted image 20240925144640.webp.md5 @@ -0,0 +1 @@ +26c46c942b83a86396577c667e1c76ff diff --git a/meta/files/images/webp/Pasted image 20240925150631.webp b/meta/files/images/webp/Pasted image 20240925150631.webp new file mode 100644 index 00000000..50999ebd Binary files /dev/null and b/meta/files/images/webp/Pasted image 20240925150631.webp differ diff --git a/meta/files/images/webp/Pasted image 20240925150631.webp.md5 b/meta/files/images/webp/Pasted image 20240925150631.webp.md5 new file mode 100644 index 00000000..092f600a --- /dev/null +++ b/meta/files/images/webp/Pasted image 20240925150631.webp.md5 @@ -0,0 +1 @@ +d7022072fd2eda793f69449c31de699a diff --git a/meta/files/zip_image.sh b/meta/files/zip_image.sh index aa909956..756c3df5 100755 --- a/meta/files/zip_image.sh +++ b/meta/files/zip_image.sh @@ -12,10 +12,6 @@ echo "Используется $THREADS потоков для обработки LOG_FILE="./zip_image_compression.log" ERROR_LOG_FILE="./zip_image_error.log" -# Инициализируем файлы логов (удалены строки, чтобы не затирать логи) -# : > "$LOG_FILE" -# : > "$ERROR_LOG_FILE" - # Экспортируем необходимые переменные и функции для использования в subshell export IMAGE_DIR COMP_DIR WEBP_DIR LOG_FILE ERROR_LOG_FILE diff --git a/meta/files/zip_image_compression.log b/meta/files/zip_image_compression.log index 96e222c1..555e958f 100644 --- a/meta/files/zip_image_compression.log +++ b/meta/files/zip_image_compression.log @@ -403,3 +403,7 @@ 2024-09-25 10:19:38 Конвертирован в WebP: ./images/Pasted image 20231120092753.png на 81,28% (621233 байт -> 116264 байт) 2024-09-25 10:19:38 Конвертирован в WebP: ./images/Pasted image 20240229204146.png на 91,84% (1343128 байт -> 109608 байт) 2024-09-25 10:19:40 Конвертирован в WebP: ./images/Pasted image 20230914145442.png на 74,19% (1371026 байт -> 353872 байт) +2024-09-25 15:14:55 Сжат PNG файл: ./images/Pasted image 20240925150631.png на 73.91% (2374543 байт -> 619599 байт) +2024-09-25 15:15:01 Сжат PNG файл: ./images/Pasted image 20240925144640.png на 75.64% (3611250 байт -> 879864 байт) +2024-09-25 15:15:06 Конвертирован в WebP: ./images/Pasted image 20240925150631.png на 67.28% (2374543 байт -> 776964 байт) +2024-09-25 15:15:08 Конвертирован в WebP: ./images/Pasted image 20240925144640.png на 68.31% (3611250 байт -> 1144526 байт)