Достижение: Добавлены скрипты и документация для релиза PDF Compressor.

- Добавлен release-body.md для подробных заметок о релизе на русском языке.
- Реализован release-gitea.ps1 для автоматизированного релиза Gitea с помощью PowerShell.
- Создан release-gitea.sh для автоматизированного релиза Gitea с помощью Bash.
- Добавлен release.sh для сборки и маркировки релизов с поддержкой нескольких платформ.
- Улучшен пользовательский интерфейс благодаря информативному логированию и обработке ошибок.
- Добавлена ​​поддержка переменных окружения и управления конфигурацией.
- Добавлена ​​функция создания архивов и загрузки ресурсов в Gitea.
This commit is contained in:
Dmitriy Fofanov
2025-11-05 09:33:12 +03:00
parent f328d67080
commit ec65cfd05a
43 changed files with 5792 additions and 2 deletions

View File

@@ -0,0 +1,259 @@
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 ""
}
}