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

345 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
tags:
- maturity/🌱
date: 2023-11-20
zero-link:
- "[[../../meta/zero/00 Снипеты на bash|00 Снипеты на bash]]"
parents:
- "[[../fundamental/Сжатие данных|Сжатие данных]]"
linked:
---
Данный скрипт предназначен для автоматического сжатия изображений форматов 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;
}
```
***
## Мета информация
**Область**:: [[../../meta/zero/00 Снипеты на bash|00 Снипеты на bash]]
**Родитель**:: [[../fundamental/Сжатие данных|Сжатие данных]]
**Источник**::
**Автор**::
**Создана**:: [[2023-11-20]]
### Дополнительные материалы
- [Преобразование изображений в Webp](Преобразование%20изображений%20в%20Webp.md)
### Дочерние заметки
<!-- QueryToSerialize: LIST FROM [[]] WHERE contains(Родитель, this.file.link) or contains(parents, this.file.link) -->