Imager — production-oriented сервис обработки изображений «на лету»: преобразует изображения по каноническим URL, кэширует результат и отдаёт его без предварительной генерации или этапа сборки. Policy-driven, предсказуемая и безопасная обработка: всё запрещено по умолчанию, разрешается только явно сконфигурированное.
GET /photos/city-skyline-jpg/300x@2.webp
→ 200 OK (WebP, 300 px wide, DPR 2), Cache-Control: public, max-age=2592000
| Возможность | Описание |
|---|---|
| Канонические URL | Детерминированные URL кодируют источник, преобразование, размер, DPR и формат вывода. Результаты неизменяемы (immutable) и дружественны к CDN |
| Пресеты и custom-размеры | Именованные преобразования (thumb@2) и размер-грамматика (640x, x400, 120x80) из конфигурации |
| Policy engine | Deny-by-default политика с longest-prefix matching по путям; жёсткие лимиты на байты, пиксели, кадры и длительность |
| Единый движок libvips | In-process обработка через CGO (govips), без subprocess; все форматы |
| AI-кроп | Детекция лиц (YuNet) и объектов (SSD MobileNet v1) через ONNX Runtime: face-crop, object-crop, face-fix, object-fix |
| Анимации | Анимированные GIF/WebP/APNG/JPEG XL/AVIF; анимированный AVIF — через нативный libheif sequence encoder |
| Видео | Извлечение кадров через ffmpeg/ffprobe (15 контейнерных форматов) для генерации превью |
| Хранилища | fs, s3, sftp, ftp/ftps, http (source-only) — независимо для источника и результата |
| Атомарная публикация | Результат публикуется в ResultStore атомарно; async-публикация с bounded-очередью |
| Singleflight | Keyed singleflight дедуплицирует одновременные запросы одного ассета |
| Admission control | Ограничение одновременных запросов с динамическим Retry-After |
| Наблюдаемость | Prometheus-метрики (/metrics), JSON-логирование (slog), /healthz и /readyz |
| Водяные знаки | Наложение с кэшированием байтов знака (LRU, TTL, инвалидация по mtime) |
| Learning-mode | Автообучение политики: наблюдения за путями накапливаются в generate-local.yaml |
Проект построен по ports-and-adapters: domain не имеет внешних
зависимостей, ports определяет интерфейсы, adapters их реализуют. Build
tags libvips и onnx переключают реализации адаптеров; без внешних
C-зависимостей компилируются заглушки, поэтому любая комбинация собирается.
flowchart LR
Client[HTTP-клиент / CDN] -->|GET asset URL| HTTP[HTTP-адаптер<br/>admission control]
HTTP --> Parse[Разбор и канонизация URL<br/>domain/asset]
Parse --> Policy[Policy engine<br/>deny-by-default]
Policy --> SF[Keyed singleflight<br/>дедупликация]
SF --> Cache{Результат<br/>в кэше?}
Cache -->|да| Out[Ответ клиенту]
Cache -->|нет| Source[SourceStore<br/>fs / s3 / sftp / ftp / http]
Source --> Plan[Построение плана<br/>обработки]
Plan --> Limits[Application limits]
Limits --> AI{AI-кроп?}
AI -->|да| ONNX[ONNX Runtime<br/>YuNet / SSD MobileNet]
AI -->|нет| Vips[libvips<br/>in-process CGO]
ONNX --> Vips
Vips --> Publish[Атомарная публикация<br/>ResultStore]
Publish --> Out
Ключевые слои:
| Слой | Назначение |
|---|---|
domain/ |
Чистая доменная логика: разбор asset URL, политика, план обработки, пресеты |
ports/ |
Контракты между слоями: storage, processor, coordinator, detector, generation |
adapters/ |
Реализации: libvips-процессор, ONNX-детекция, хранилища, ffmpeg, HTTP-адаптер |
app/ |
Application use cases: generatev2 (генерация ассетов), adminsvc, learning |
coordination/ |
In-process keyed singleflight |
observability/ |
Метрики, логирование, middleware |
composition/ |
Сборка приложения, загрузка конфигурации |
Без клонирования репозитория и сборки — только готовый образ
altrap/imager и ваши каталоги с данными. Базовые конфиги
(server.yaml, generate.yaml, failback.yaml) уже в образе: при старте
entrypoint подтянет их в смонтированный каталог конфигурации, если там их
нет.
# 1. Каталоги: конфигурация (можно пустой), исходники, результаты
mkdir -p setting data/source data/result
chmod -R a+rwX data/result # запись нужна uid 10001 (imager)
docker run -d --name imager -p 8080:8080 \
-v ./setting:/etc/imager/setting:rw \
-v ./data/source:/data/source:ro \
-v ./data/result:/data/result:rw \
-e IMAGER_CONFIG_DIR=/etc/imager/setting \
altrap/imager:latest
curl http://localhost:8080/healthz # {"status":"alive"}
curl -o out.webp http://localhost:8080/test-jpg/x.webp # ассет в webpОбязательные volumes: setting (конфигурация; пустой каталог заполнится
дефолтами образа при старте), data/source (исходники), data/result
(результаты). Опциональные: ./models:/etc/imager/models:rw — каталог
ONNX-моделей (entrypoint скачает их при старте). Порт — 8080 (plain HTTP,
TLS — на reverse-proxy).
Переопределение конфигурации — два способа:
- Только
*-local.yaml(рекомендуется): монтируйте пустой./settingи кладите туда толькоserver-local.yaml/generate-local.yaml/failback-local.yaml— они глубоко мержатся поверх базовых конфигов образа (см. docs/CONFIGURATION.md); - Все конфиги целиком: положите в
./settingполный наборserver.yaml+generate.yaml+failback.yaml(+*-local.yaml) — они полностью заменят дефолты образа.
Минимальный вариант (production-опции закомментированы):
services:
imager:
image: altrap/imager:latest
restart: unless-stopped
stop_signal: INT
stop_grace_period: 15s
ports:
- "8080:8080"
environment:
IMAGER_CONFIG_DIR: /etc/imager/setting
volumes:
- ./setting:/etc/imager/setting:rw
- ./data/source:/data/source:ro
- ./data/result:/data/result:rw
# - ./models:/etc/imager/models:rw # опционально (ONNX-модели)docker compose up -dProduction-вариант с hardening (лимиты ресурсов, tmpfs, dropped capabilities,
no-new-privileges) — docker-compose.yaml в корне
репозитория. Подробнее — docs/DEPLOYMENT.md.
Требуется Go ≥ 1.27. Сборка по умолчанию использует процессоры-заглушки и подходит для разработки и CI:
go build -o imager ./cmd/imager
IMAGER_CONFIG_DIR=./setting ./imagerПродакшен-сборка включает libvips (нужен CGO):
# Debian/Ubuntu: sudo apt-get install libvips-dev build-essential pkg-config
go build -tags libvips -trimpath -ldflags="-s -w" -o imager ./cmd/imagerС детекцией лиц/объектов (ONNX Runtime):
go build -tags "libvips,onnx" -trimpath -ldflags="-s -w" -o imager ./cmd/imagerВсе варианты сборки и зависимости кодеков — в docs/INSTALLATION.md.
Единая грамматика для канонических и preset URL:
/{path}/{source_name}-{source_format}/{segment}@{dpr}.{output_format}
| Компонент | Описание |
|---|---|
path |
Логический путь исходника в хранилище (до 512 символов) |
source_name |
Имя исходного файла без расширения (до 128 символов) |
source_format |
Формат исходника: jpeg, png, webp, gif, avif, heif, apng, jxl или видео-контейнер (mp4, webm, mov, …) |
segment |
Имя пресета (thumb) или custom-размер (640x, x400, 120x80, x) |
dpr |
Device pixel ratio: отсутствие = 1, явно допустимы только 2 и 3 |
output_format |
Выходной формат: jpeg, png, webp, gif, avif, heif, jxl |
Примеры (исходник test.jpg в корне source-хранилища):
# Пресет thumb (200x200, если задан в path-policy "/")
curl -o thumb.webp http://localhost:8080/test-jpg/thumb.webp
# Пресет thumb@2 (dpr фиксирован именем)
curl -o thumb2.webp http://localhost:8080/test-jpg/thumb@2.webp
# Custom: ширина 640
curl -o out.webp http://localhost:8080/test-jpg/640x.webp
# Custom: только высота 400
curl -o out.png http://localhost:8080/test-jpg/x400.png
# Custom: исходный размер, конвертация в AVIF
curl -o out.avif http://localhost:8080/test-jpg/x.avif
# Custom 120x80@2 с DPR 2 (реально 240x160)
curl -o out.webp http://localhost:8080/test-jpg/120x80@2.webp
# С путём: исходник thumbs/photo.jpg
curl -o out.webp http://localhost:8080/thumbs/photo-jpg/thumb.webp
# Условный запрос (ETag из первого ответа)
curl -I -H 'If-None-Match: "etag-from-first-response"' \
http://localhost:8080/test-jpg/thumb.webp # 304Канонический ключ кэша — сам canonical URL: закэшированный ассет доступен по человекочитаемому имени. Полный справочник — docs/API.md.
Единственный движок — libvips (in-process, CGO, без subprocess). Запрос
проходит конвейер app/generatev2:
- Разбор URL и валидация (
domain/asset). - Разрешение пресета/custom в канонический запрос.
- Проверка политики (deny-by-default) и application-лимитов.
- Fast-path оригинала (
size=x, без transform, формат = исходному) — файл отдаётся как есть, без обработки и без зачистки метаданных. - Поиск готового результата в ResultStore по каноническому ключу.
- Keyed singleflight: параллельные запросы того же ассета дедуплицируются.
- Открытие источника и построение плана обработки — параллельно.
- Обработка движком в spillable-буфер (память с переполнением на диск).
- Атомарная публикация результата в ResultStore.
Операции задаются полем crop пресета/custom; trim — независимый фильтр
обрезки однотонных полей. Порядок применения:
auto-orient → rotate → flip → trim → crop/resize.
| Операция | crop |
Описание |
|---|---|---|
| Resize | "" |
Изменение размера с сохранением пропорций; letterbox/pillarbox при двух осях |
| Crop | center |
Центрированная обрезка до целевого размера |
| Smart-crop | smart |
Обрезка по attention-области (libvips) |
| Face-crop | face |
Обрезка по обнаруженным лицам (ONNX YuNet) |
| Object-crop | object |
Обрезка по обнаруженным объектам (ONNX SSD) |
| Face-fix | face-fix |
Cover-масштаб со сдвигом к лицу, без зума |
| Object-fix | object-fix |
Cover-масштаб со сдвигом к объекту, без зума |
Подробности — docs/PROCESSING.md.
Источник и результат настраиваются независимо (source / result в
server.yaml):
| Тип | Роль | Особенности |
|---|---|---|
fs |
source / result | Локальная ФС; secure open (openat2 с RESOLVE_BENEATH на Linux), атомарная публикация (temp + rename + fsync), квоты, janitor |
s3 |
source / result | S3 и S3-совместимые (MinIO, Yandex Object Storage, …); пул соединений, retry, кэш метаданных |
sftp |
source / result | SSH File Transfer Protocol; обязательный host-key-fingerprint |
ftp / ftps |
source / result | FTP и FTP over TLS (explicit); tls-verify: false запрещён для ftps |
http |
только source | HTTP/HTTPS чтение исходников; использование как result — ошибка старта |
Ключи объектов во всех хранилищах нормализуются: запрет .., обратных
слешей, NUL и управляющих байтов. Подробности — docs/STORAGE.md.
Deny-by-default: всё запрещено по умолчанию, разрешается только явно
перечисленное в path-policies. Выбор правила — longest-prefix match,
"/" — fallback для всех путей. Политика компилируется в неизменяемую
структуру на старте (fail-fast при невалидных правилах).
policy:
presets:
thumb:
width: 200
height: 200
output-formats: [webp, avif]
quality: 85
dpr: 1
thumb@2:
width: 200
height: 200
output-formats: [webp, avif]
quality: 85
dpr: 2
path-policies:
# "/" — fallback для всех путей
/:
presets: ["thumb", "thumb@2"]
customs:
x:
output-formats: [webp]
x200:
output-formats: [webp, avif]
200x200:
output-formats: [webp]
# /thumbs — специфичный префикс
/thumbs:
presets: ["thumb"]
customs:
100x100:
output-formats: [webp, avif]Отклонение запроса → 403 forbidden. Лимиты (application.limits) —
source-bytes, output-bytes, width, height, pixels, dpr, frames,
duration, concurrency — применяются к любому запросу независимо от
политики. Подробности — docs/SECURITY.md и
docs/CONFIGURATION.md.
Детекция выполняется ONNX-моделями внутри процесса (сборка с
-tags libvips,onnx):
| Модель | Операции | Назначение |
|---|---|---|
YuNet (face-model) |
face, face-fix |
Детекция лиц |
SSD MobileNet v1 (object-model) |
object, object-fix |
Детекция объектов |
detection:
face-model: "/etc/imager/models/face_detection_yunet_2023mar.onnx"
object-model: "/etc/imager/models/ssd_mobilenet_v1_12.onnx"
confidence-threshold: 0.4 # порог уверенности [0,1]
max-objects: 15 # максимум объектов после NMS
margin: 0.2 # отступ вокруг бокса как доля его размераСвойства:
- модели загружаются лениво при первом запросе и кэшируются в памяти;
- результаты детекции кэшируются в sidecar-хранилище метаданных: модель вызывается ровно один раз на родительский файл;
- ONNX-инференс выполняется под отдельным detection-семафором (handoff с libvips-слотом), чтобы не голодать лёгкие операции;
- при перегрузке AI-детекции запрос деградирует к center-crop (graceful degradation), а не получает 503.
Подробности — docs/PROCESSING.md.
| Формат | Анимированный вход | Анимированный выход |
|---|---|---|
| GIF | да | да |
| WebP | да | да |
| APNG | да | нет — запись требует libvips, собранного с libspng (см. docs/INSTALLATION.md) |
| JPEG XL | да | да |
| AVIF | да | да — нативный libheif sequence encoder (настоящий animation track, libheif ≥ 1.23) |
| HEIF/HEIC | да | нет — выход содержит только первый кадр (ограничение libheif 1.23, не ошибка входа) |
Анимация определяется как Pages() > 1 && len(delay) > 0 (наличие frame
timing). Лимиты кадров/длительности задаются в пресетах (frames,
duration) и application.limits.
Видео-контейнеры декодируются через ffmpeg/ffprobe для извлечения
одного кадра (превью/ассет), а не полноценного видео-кодирования.
Поддерживаются 15 форматов: mp4, webm, mov, mkv, avi, m4v,
mpg, mpeg, wmv, flv, 3gp, ogv, ts, mts, m2ts.
Кадр выбирается по проценту от длительности (default-video-frame-percent),
с проверкой контрастности и поиском следующего кандидата. Извлечённый кадр
кэшируется как x.jpg. Подробности —
docs/PROCESSING.md.
| Механизм | Описание |
|---|---|
| Deny-by-default политика | Разрешено только явно покрытое path-policies; отклонение → 403 |
| Безопасность URL | Парсер отклоняет traversal (..), encoded-разделители (%2f), control-символы, обратные слеши; лимиты длины компонентов |
| Защита ФС | Secure open (openat2 RESOLVE_BENEATH на Linux), запрет symlink-обхода, атомарная публикация, квоты |
| HTTP hardening | Security headers, CORS deny-by-default, таймауты (slowloris), лимиты заголовков/тела/URL, panic recovery |
| Admission control | 503 + динамический Retry-After при перегрузке; health/metrics остаются доступными |
| Singleflight | Дедупликация конкурентных запросов; таймаут ожидания владельца → 503 |
| Секреты | Только в *-local.yaml (не коммитятся); S3-credentials через env |
| Admin | Выключен по умолчанию; при включении обязателен непустой bearer-токен (constant-time сравнение) |
Подробности — docs/SECURITY.md.
- In-process libvips — без subprocess и IPC-оверхеда; CGO-привязка govips.
- Кэширование результата — повторные запросы отдаются из ResultStore без
обработки;
Cache-Control: immutableи ETag/304 для CDN и браузеров. - Keyed singleflight — одновременные запросы одного ассета выполняются один раз, остальные ждут результат (до 16384 одновременных ключей).
- Async-публикация — запись в remote (fsync/upload с retry) вынесена из критического пути: bounded-очередь (512 задач, 4 воркера, drain 5 с); при переполнении — синхронный fallback, результаты не теряются.
- Shrink-on-load — предварительное уменьшение при декодировании JPEG/WebP/GIF/HEIF/AVIF по целевому размеру.
- Спиллабл-буферы — память с переполнением на диск при исчерпании
бюджета
application.buffer-max-bytes(дефолт 500 MiB). - Лимиты ресурсов —
libvips.limits:timeout,source-bytes,output-bytes,concurrency,threads, лимиты кэша.
| Эндпоинт | Назначение |
|---|---|
/healthz |
Liveness: 200 {"status":"alive"} / 503 {"status":"dead"} |
/readyz |
Readiness: 200 {"status":"ready"} / 503 {"status":"not_ready"} |
/metrics |
Метрики в Prometheus exposition format (expvar; может быть защищён токеном/IP) |
- Метрики — bounded-cardinality счётчики и гистограммы по стадиям
request/cache/processor/storage:
imager_requests_*,imager_cache_*,imager_processor_*,imager_storage_*,imager_vips_*,imager_publish_errors_total,imager_detection_degraded_totalи др. URL/query/секреты в метрики не попадают. - Логирование — структурированное JSON-логирование (slog), request ID
(
X-Request-Id), уровниdebug/info/warn/error. - Asset errors — счётчики ошибок asset URL, bounded top-paths (LRU).
Все настройки задаются в YAML; CLI-флагов у приложения нет. Конфигурация
разделена на три слоя, каждый переопределяется файлом -local.yaml
(игнорируется git):
| Слой | Файлы | Содержимое |
|---|---|---|
| setting | server.yaml + server-local.yaml |
Сервер, хранилища, libvips, encoders, detection, лимиты, наблюдаемость, admin |
| generate | generate.yaml + generate-local.yaml |
Пресеты, политика, водяные знаки, детекция |
| failback | failback.yaml + failback-local.yaml |
not-found fallback, source-fallback |
Переменные окружения:
| Переменная | Назначение |
|---|---|
IMAGER_CONFIG_DIR |
Каталог с файлами конфигурации |
IMAGER_MODELS_DIR |
Каталог ONNX-моделей (fallback для detection.face-model/object-model) |
IMAGER_S3_ACCESS_KEY / IMAGER_S3_SECRET_KEY |
S3-credentials (значение из YAML приоритетнее) |
Полный справочник — docs/CONFIGURATION.md, примеры
с комментариями — в setting/.
- Docker — multi-stage сборка, Alpine 3.24, non-root (uid 10001),
pinned-версии пакетов, entrypoint с force-sync базовых конфигов при смене
релиза. Образ:
altrap/imager(Docker Hub). - Hardening —
docker-compose.yaml: лимиты ресурсов, tmpfs для/tmp, dropped capabilities, no-new-privileges. - Reverse-proxy — TLS и кэширование на NGINX: docs/NGINX.md.
- Production-рекомендации — docs/DEPLOYMENT.md.
| Ресурс | Содержимое |
|---|---|
| Демо | Онлайн-пример работы сервиса и клиентской части |
| imager-client | Клиент для формирования asset URL |
| docs/API.md | Формат URL, эндпоинты, заголовки, ошибки |
| docs/CONFIGURATION.md | Полный справочник конфигурации |
| docs/INSTALLATION.md | Зависимости и инструкции по сборке |
| docs/DEPLOYMENT.md | Продакшен-развёртывание, защита контейнера |
| docs/PROCESSING.md | Конвейер обработки, операции, водяные знаки, анимации |
| docs/STORAGE.md | Бэкенды хранилищ и их настройки |
| docs/SECURITY.md | Политика, лимиты, безопасность URL и ФС |
| docs/NGINX.md | Настройка reverse-proxy |
| docs/ARCHITECTURE.md | Общая архитектура, жизненный цикл запроса, слои приложения |
| docs/POLICIES.md | Модель политики доступа (deny-by-default) |
| docs/FORMATS.md | Поддерживаемые форматы, анимации, видео |
| docs/AI.md | AI-кропы, детекция лиц и объектов (ONNX) |
| docs/OBSERVABILITY.md | Метрики, логи, health-эндпоинты |
| docs/DEVELOPMENT.md | Разработка, сборка, тестирование, CI/CD |
| docs/TROUNLESHOOTING.md | Диагностика типичных проблем |
policy:
presets:
banner:
width: 1200
height: 400
output-formats: [webp, avif]
quality: 80
frames: 0 # 0 = без ограничения кадров
duration: 0 # 0 = без ограничения длительности
loop: true
dpr: 1
avatar:
crop: center
width: 100
height: 100
output-formats: [webp]
portrait:
crop: face
trim: true
width: 300
height: 300
output-formats: [jpeg]watermarks:
logo:
path: "/etc/imager/watermarks/logo.png"
position: center # top | bottom | left | right | center
repeat: no-repeat # no-repeat | repeat | repeat-x | repeat-y | round | space
size: contain # contain | cover | "200px 50px" | "50%"
opacity: 100 # 0-100; 100 = непрозрачныйhttp:
not-found:
pixel: true # прозрачный 1x1 пиксель в запрошенном формате
# image: "not-found.png"
# page: "not-found.html"
# redirect: "https://example.com/404"
source-fallback:
enabled: false
status: 404
cache-control: "no-store"На Linux/macOS (без установки libvips/onnxruntime на хост — через
предварительно собранный CI-образ
.gitverse/docker/imager-ci):
make docker-test # go test -tags "libvips onnx" ./...
make docker-test-race # go test -race -tags "libvips onnx" ./...
make docker-check # fmt-check + test + race + govulncheck (как CI)
make docker-govulncheck # govulncheck ./...Локально (требуются libvips + ONNX Runtime на хосте):
make install # download and tidy modules
make build # сборка с libvips + onnx
make run # сборка + запуск с IMAGER_CONFIG_DIR=./setting
make vet # go vet
make fmt # gofmt
make check # fmt + vet + test + raceНа Windows — PowerShell-раннер make.ps1 (аналог Makefile):
.\make.ps1 install # go mod download + tidy
.\make.ps1 test # go test ./...
.\make.ps1 docker-test # go test (libvips,onnx) в CI-образе (Docker Desktop)
.\make.ps1 check # fmt-check + vet + test + race
.\make.ps1 help # список всех целейmake test # go test ./... -count=1
make race # go test -race ./... -count=1
make fuzz # fuzz smoke-тесты (FuzzParse, FuzzParseSize)
make tags-check # все комбинации build tags (default/onnx/libvips,onnx)CI (.gitverse/workflows/ci.yml) собирает и
тестирует все комбинации build tags (default/onnx на Linux и Windows,
libvips/libvips,onnx на Linux), запускает go vet, go test -race
(Linux), gofmt, govulncheck, fuzz smoke-тесты и сканирование контейнера
Trivy (GitHub Actions, Docker Hub).
© 2025 Алтухов Владислав Владимирович.
Проект распространяется по лицензии GNU General Public License v3.0.