Базовый пакет imagemagick в alpine идёт без кодеков, поэтому magick не мог прочитать jpeg-вложение и падал с "exit status 1". Ставим imagemagick-jpeg, -heic, -webp, -tiff. Чтобы такое было видно из логов: runMagick подставляет stderr в ошибку и отдельно ловит "код выхода 0, но пустой вывод" (так magick ведёт себя без кодека на запись), а веб логирует формат, размеры до и после, время скачивания и ресайза. Если ресайз всё же не удался, отдаём оригинал вместо 502; в кэш не кладём записи больше 4 МБ, чтобы неужатые оригиналы не съели память. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
111 lines
3.4 KiB
Go
111 lines
3.4 KiB
Go
package media
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"os/exec"
|
|
"strings"
|
|
)
|
|
|
|
const maxTelegramPhotoSize = 10 * 1024 * 1024 // 10 MB
|
|
|
|
// webImageMaxSide — максимальная сторона картинки, отдаваемой на веб-страницу.
|
|
// Экран всё равно не покажет больше, а исходники с телефона весят десятки мегабайт.
|
|
const webImageMaxSide = 1600
|
|
|
|
// CompressImage compresses an image if it exceeds Telegram's 10 MB photo limit.
|
|
// It uses ImageMagick's magick command to convert via stdin/stdout.
|
|
// Returns the (possibly compressed) data, updated filename, and any error.
|
|
func CompressImage(ctx context.Context, data []byte, filename string) (out []byte, outName string, err error) {
|
|
if len(data) <= maxTelegramPhotoSize {
|
|
return data, filename, nil
|
|
}
|
|
|
|
// First attempt: just re-encode as JPEG with quality 85
|
|
out, err = runMagick(ctx, data, "-quality", "85")
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("compress image: %w", err)
|
|
}
|
|
|
|
newFilename := replaceExt(filename, ".jpg")
|
|
|
|
if len(out) <= maxTelegramPhotoSize {
|
|
return out, newFilename, nil
|
|
}
|
|
|
|
// Second attempt: resize to 50% and quality 85
|
|
out, err = runMagick(ctx, data, "-resize", "50%", "-quality", "85")
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("compress image with resize: %w", err)
|
|
}
|
|
|
|
if len(out) > maxTelegramPhotoSize {
|
|
return nil, "", fmt.Errorf("image still too large after compression (%d bytes)", len(out))
|
|
}
|
|
|
|
return out, newFilename, nil
|
|
}
|
|
|
|
// ResizeForWeb downscales an image to webImageMaxSide and re-encodes it as JPEG.
|
|
// Images smaller than the limit are only re-encoded (ImageMagick's ">" modifier
|
|
// never enlarges). Returns the data and the resulting MIME type.
|
|
func ResizeForWeb(ctx context.Context, data []byte, mimeType string) ([]byte, string, error) {
|
|
// GIF (анимация) и SVG (вектор) при конверсии в JPEG теряют смысл — отдаём как есть.
|
|
if !resizableMIME(mimeType) {
|
|
return data, mimeType, nil
|
|
}
|
|
|
|
out, err := runMagick(ctx, data,
|
|
"-auto-orient",
|
|
"-resize", fmt.Sprintf("%dx%d>", webImageMaxSide, webImageMaxSide),
|
|
"-quality", "85",
|
|
"-strip",
|
|
)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("resize image: %w", err)
|
|
}
|
|
|
|
return out, "image/jpeg", nil
|
|
}
|
|
|
|
func resizableMIME(mimeType string) bool {
|
|
switch mimeType {
|
|
case "image/gif", "image/svg+xml":
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func runMagick(ctx context.Context, data []byte, args ...string) ([]byte, error) {
|
|
cmdArgs := append([]string{"-"}, args...)
|
|
cmdArgs = append(cmdArgs, "jpeg:-")
|
|
|
|
cmd := exec.CommandContext(ctx, "magick", cmdArgs...)
|
|
cmd.Stdin = bytes.NewReader(data)
|
|
|
|
// Без stderr от magick ошибка выглядит как голое "exit status 1"
|
|
// и не подсказывает, чего не хватает (обычно — кодека).
|
|
var stderr bytes.Buffer
|
|
cmd.Stderr = &stderr
|
|
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("magick: %w: %s", err, strings.TrimSpace(stderr.String()))
|
|
}
|
|
|
|
// Без кодека magick иногда завершается успешно, но не пишет ничего.
|
|
if len(out) == 0 {
|
|
return nil, fmt.Errorf("magick produced empty output: %s", strings.TrimSpace(stderr.String()))
|
|
}
|
|
|
|
return out, nil
|
|
}
|
|
|
|
func replaceExt(filename, newExt string) string {
|
|
if i := strings.LastIndex(filename, "."); i >= 0 {
|
|
return filename[:i] + newExt
|
|
}
|
|
return filename + newExt
|
|
}
|