Files
compress/internal/infrastructure/compressors/image_compressor.go
Dmitriy Fofanov ec65cfd05a Достижение: Добавлены скрипты и документация для релиза PDF Compressor.
- Добавлен release-body.md для подробных заметок о релизе на русском языке.
- Реализован release-gitea.ps1 для автоматизированного релиза Gitea с помощью PowerShell.
- Создан release-gitea.sh для автоматизированного релиза Gitea с помощью Bash.
- Добавлен release.sh для сборки и маркировки релизов с поддержкой нескольких платформ.
- Улучшен пользовательский интерфейс благодаря информативному логированию и обработке ошибок.
- Добавлена ​​поддержка переменных окружения и управления конфигурацией.
- Добавлена ​​функция создания архивов и загрузки ресурсов в Gitea.
2025-11-05 09:33:12 +03:00

260 lines
8.5 KiB
Go
Raw 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.

package compressors
import (
"fmt"
"image"
"image/jpeg"
"image/png"
"io"
"os"
"path/filepath"
"strings"
"github.com/nfnt/resize"
)
// ImageCompressor интерфейс для сжатия изображений
type ImageCompressor interface {
CompressJPEG(inputPath, outputPath string, quality int) error
CompressPNG(inputPath, outputPath string, quality int) error
}
// DefaultImageCompressor реализация компрессора изображений
type DefaultImageCompressor struct{}
// NewImageCompressor создает новый компрессор изображений
func NewImageCompressor() ImageCompressor {
return &DefaultImageCompressor{}
}
// CompressJPEG сжимает JPEG файл с указанным качеством
func (c *DefaultImageCompressor) CompressJPEG(inputPath, outputPath string, quality int) error {
// Открываем исходный файл
inputFile, err := os.Open(inputPath)
if err != nil {
return fmt.Errorf("не удалось открыть файл %s: %w", inputPath, err)
}
defer inputFile.Close()
// Декодируем изображение
img, err := jpeg.Decode(inputFile)
if err != nil {
return fmt.Errorf("не удалось декодировать JPEG файл %s: %w", inputPath, err)
}
// Получаем размер исходного файла для сравнения
inputFileInfo, err := inputFile.Stat()
if err != nil {
return fmt.Errorf("не удалось получить информацию о файле %s: %w", inputPath, err)
}
originalSize := inputFileInfo.Size()
// Вычисляем новый размер на основе качества
bounds := img.Bounds()
width := bounds.Dx()
height := bounds.Dy()
// Более агрессивное уменьшение размера для достижения реального сжатия
// quality 10 -> 0.5 (50%), quality 50 -> 0.9 (90%)
scaleFactor := 0.5 + float64(quality-10)/40.0*0.4
if scaleFactor > 1.0 {
scaleFactor = 1.0
}
newWidth := uint(float64(width) * scaleFactor)
newHeight := uint(float64(height) * scaleFactor)
// Изменяем размер изображения только если есть реальная польза
var finalImg image.Image
if newWidth < uint(width) && newHeight < uint(height) {
finalImg = resize.Resize(newWidth, newHeight, img, resize.Lanczos3)
} else {
finalImg = img
}
// Маппинг качества: 10->30, 30->55, 50->75 (более консервативно)
jpegQuality := 20 + int(float64(quality-10)/40.0*55.0)
if jpegQuality < 20 {
jpegQuality = 20
}
if jpegQuality > 75 {
jpegQuality = 75
}
// Создаем временный файл для проверки результата
tmpPath := outputPath + ".tmp"
tmpFile, err := os.Create(tmpPath)
if err != nil {
return fmt.Errorf("не удалось создать временный файл: %w", err)
}
// Кодируем во временный файл
options := &jpeg.Options{Quality: jpegQuality}
err = jpeg.Encode(tmpFile, finalImg, options)
tmpFile.Close()
if err != nil {
os.Remove(tmpPath)
return fmt.Errorf("не удалось закодировать JPEG: %w", err)
}
// Проверяем размер результата
tmpInfo, err := os.Stat(tmpPath)
if err != nil {
os.Remove(tmpPath)
return fmt.Errorf("не удалось получить информацию о временном файле: %w", err)
}
// Если сжатие неэффективно (файл больше или почти такой же), копируем оригинал
if tmpInfo.Size() >= originalSize*95/100 {
os.Remove(tmpPath)
// Копируем оригинал
inputFile.Seek(0, 0)
outputFile, err := os.Create(outputPath)
if err != nil {
return fmt.Errorf("не удалось создать выходной файл: %w", err)
}
defer outputFile.Close()
_, err = io.Copy(outputFile, inputFile)
if err != nil {
return fmt.Errorf("не удалось скопировать файл: %w", err)
}
return nil
}
// Переименовываем временный файл в выходной
err = os.Rename(tmpPath, outputPath)
if err != nil {
os.Remove(tmpPath)
return fmt.Errorf("не удалось переименовать временный файл: %w", err)
}
return nil
}
// CompressPNG сжимает PNG файл с указанным качеством
func (c *DefaultImageCompressor) CompressPNG(inputPath, outputPath string, quality int) error {
// Открываем исходный файл
inputFile, err := os.Open(inputPath)
if err != nil {
return fmt.Errorf("не удалось открыть файл %s: %w", inputPath, err)
}
defer inputFile.Close()
// Получаем размер исходного файла для сравнения
inputFileInfo, err := inputFile.Stat()
if err != nil {
return fmt.Errorf("не удалось получить информацию о файле %s: %w", inputPath, err)
}
originalSize := inputFileInfo.Size()
// Декодируем изображение
img, err := png.Decode(inputFile)
if err != nil {
return fmt.Errorf("не удалось декодировать PNG файл %s: %w", inputPath, err)
}
// Вычисляем новый размер на основе качества
bounds := img.Bounds()
width := bounds.Dx()
height := bounds.Dy()
// Более консервативное масштабирование для PNG
// quality 10 -> 0.6 (60%), quality 50 -> 0.9 (90%)
scaleFactor := 0.6 + float64(quality-10)/40.0*0.3
if scaleFactor > 1.0 {
scaleFactor = 1.0
}
newWidth := uint(float64(width) * scaleFactor)
newHeight := uint(float64(height) * scaleFactor)
// Не изменяем размер для маленьких изображений
if width < 400 && height < 400 {
newWidth = uint(width)
newHeight = uint(height)
}
// Изменяем размер изображения только если это даст выигрыш
var finalImg image.Image
if newWidth < uint(width) && newHeight < uint(height) {
finalImg = resize.Resize(newWidth, newHeight, img, resize.Lanczos3)
} else {
finalImg = img
}
// Создаем временный файл для проверки результата
tmpPath := outputPath + ".tmp"
tmpFile, err := os.Create(tmpPath)
if err != nil {
return fmt.Errorf("не удалось создать временный файл: %w", err)
}
// Для PNG используем максимальное сжатие
encoder := &png.Encoder{
CompressionLevel: png.BestCompression,
}
err = encoder.Encode(tmpFile, finalImg)
tmpFile.Close()
if err != nil {
os.Remove(tmpPath)
return fmt.Errorf("не удалось закодировать PNG: %w", err)
}
// Проверяем размер результата
tmpInfo, err := os.Stat(tmpPath)
if err != nil {
os.Remove(tmpPath)
return fmt.Errorf("не удалось получить информацию о временном файле: %w", err)
}
// Если сжатие неэффективно (файл больше или почти такой же), копируем оригинал
if tmpInfo.Size() >= originalSize*95/100 {
os.Remove(tmpPath)
// Копируем оригинал
inputFile.Seek(0, 0)
outputFile, err := os.Create(outputPath)
if err != nil {
return fmt.Errorf("не удалось создать выходной файл: %w", err)
}
defer outputFile.Close()
_, err = io.Copy(outputFile, inputFile)
if err != nil {
return fmt.Errorf("не удалось скопировать файл: %w", err)
}
return nil
}
// Переименовываем временный файл в выходной
err = os.Rename(tmpPath, outputPath)
if err != nil {
os.Remove(tmpPath)
return fmt.Errorf("не удалось переименовать временный файл: %w", err)
}
return nil
}
// IsImageFile проверяет, является ли файл изображением поддерживаемого формата
func IsImageFile(filename string) bool {
ext := strings.ToLower(filepath.Ext(filename))
return ext == ".jpg" || ext == ".jpeg" || ext == ".png"
}
// GetImageFormat возвращает формат изображения по расширению файла
func GetImageFormat(filename string) string {
ext := strings.ToLower(filepath.Ext(filename))
switch ext {
case ".jpg", ".jpeg":
return "jpeg"
case ".png":
return "png"
default:
return ""
}
}