Struchkov Mark
190ea59670
All checks were successful
continuous-integration/drone/push Build is passing
345 lines
17 KiB
Markdown
345 lines
17 KiB
Markdown
---
|
||
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) -->
|