- Добавлен release-body.md для подробных заметок о релизе на русском языке. - Реализован release-gitea.ps1 для автоматизированного релиза Gitea с помощью PowerShell. - Создан release-gitea.sh для автоматизированного релиза Gitea с помощью Bash. - Добавлен release.sh для сборки и маркировки релизов с поддержкой нескольких платформ. - Улучшен пользовательский интерфейс благодаря информативному логированию и обработке ошибок. - Добавлена поддержка переменных окружения и управления конфигурацией. - Добавлена функция создания архивов и загрузки ресурсов в Gitea.
260 lines
8.5 KiB
Go
260 lines
8.5 KiB
Go
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 ""
|
||
}
|
||
}
|