Водяные знаки для интернет-магазина: кейс пакетной обработки сотен тысяч изображений
2014-05-25
Это старый, но показательный кейс из практики: нужно было наладить обработку нескольких сотен тысяч изображений для интернет-магазинов, где на выходе требовались водяные знаки, ресайз и предсказуемый массовый процесс без ручной работы.
1. Кейс с точки зрения бизнеса
Задача была не в "красивой картинке в фотошопе", а в воспроизводимом пайплайне для каталога:
- обрабатывать большие объёмы изображений (сотни тысяч файлов),
- накладывать водяной знак так, чтобы он читался на разных фонах,
- делать ресайз для нужных размеров карточек/каталога,
- складывать готовые результаты в AWS S3,
- обойтись без оконного интерфейса и ручной работы дизайнеров.
Отдельное ограничение: магазинов было много, поэтому водяные знаки нужно было генерировать под разные названия и использовать дальше в автоматическом режиме.
В итоге получился batch-процесс, который можно было запускать на больших массивах данных и получать однотипный результат без "человеческого фактора".
2. Поиск решения: какой водяной знак вообще работает
Ниже - путь подбора решения на примерах. Цель была практичная: сделать знак заметным, но не убить читаемость изображения и текста на нём.
Почему обычная полупрозрачная надпись работает плохо
Белая полупрозрачная надпись теряется на светлом фоне, тёмная - на тёмном. Один цвет не даёт стабильного результата.
Рабочий вариант: светлая надпись + тёмная полупрозрачная тень
Лучше сработала комбинация из белой полупрозрачной надписи и чёрной полупрозрачной тени. Такой знак заметен и на светлых, и на тёмных участках.
Если на изображении есть текст, важно не перекрыть его полностью. Комбинация прозрачности надписи и тени помогает сохранить читаемость.
Почему один знак по центру - слабая защита
Если знак один и всегда в одном месте (обычно по центру), его сравнительно легко убрать локальной обработкой и наложить новый поверх.
Итоговый подход: замощение изображения
Чтобы усложнить удаление, знак размещался не точечно, а сеткой по всей площади изображения. Центральный элемент служил точкой привязки, остальные раскладывались вокруг. Поворот надписи делал результат визуально аккуратнее.
Автор картинок с ягодами и апельсином - Кирилл Краснов. Shutterstock: http://www.shutterstock.com/cat.mhtml?gallery_id=419308.
3. Технические детали
Это реализация 2014 года. На тот момент Cloudflare Images ещё не существовал, а сегодня для многих подобных задач логично брать решение "из коробки" (хранение, ресайз/трансформации, доставка). В этом кейсе собственная реализация остаётся в презентативных целях: показать подход к batch-обработке, когда нужно было собрать всё вручную.
Тогда пайплайн выглядел так:
- сгенерировать PNG-водяной знак для конкретного магазина,
- прочитать исходное изображение,
- сделать ресайз под нужные размеры,
- наложить плитку водяных знаков,
- сохранить результат и выгрузить в AWS S3.
3.1. Генерация водяного знака (Perl + ImageMagick)
Ниже - скрипт генерации PNG-водяного знака с тенью и поворотом. Это основа для дальнейшего замощения.
#!/usr/bin/perl -w
use strict;
use Image::Magick;
die `pod2text $0` unless @ARGV;
# Создаём холст с прозрачным фоном
my $image = Image::Magick->new(size=>'1000x70');
$image->ReadImage('canvas:transparent');
# Наносим надпись чёрным цветом с прозрачностью 30%
$image->Annotate(
text => $ARGV[0],
geometry => "+50+50",
pen => $image->QueryColorname('rgba(0,0,0,0.3)'),
font => 'Bookman-Demi',
pointsize => 40,
kerning => 3,
);
# Размываем - это будет тень
$image->Blur(
radius => 0,
sigma => 6,
channel => 'RGBA'
);
# Создаём маску под саму надпись
my $mask = Image::Magick->new(size=>'1000x70');
$mask->ReadImage('canvas:transparent');
$mask->Annotate(
text => $ARGV[0],
geometry => "+50+50",
pen => $image->QueryColorname('rgba(255,255,255,1)'),
font => 'Bookman-Demi',
pointsize => 40,
kerning => 3,
);
# Очищаем центр тени по маске, чтобы не было "грязи" под белым текстом
$image->Composite(
image => $mask,
mask => $mask,
compose => 'Clear',
);
# Наносим белую полупрозрачную надпись поверх
$image->Annotate(
text => $ARGV[0],
geometry => "+50+50",
pen => $image->QueryColorname('rgba(255,255,255,0.3)'),
font => 'Bookman-Demi',
pointsize => 40,
kerning => 3,
);
$image->Trim();
# Поворачиваем надпись
$image->Rotate(
degrees => -45,
background => 'transparent',
);
# Сохраняем в PNG
if ($ARGV[1]) {
$image->Write("$ARGV[1]");
}
else {
$image->Write("$ARGV[0].png");
}3.2. Наложение плитки водяных знаков на изображение
Второй скрипт вычисляет сетку, центрирует её относительно исходной картинки и замащивает изображение водяными знаками.
#!/usr/bin/perl -w
use strict;
use Image::Magick;
use POSIX qw/ceil/;
die `pod2text $0` unless @ARGV;
# Исходная картинка
my $image = Image::Magick->new;
$image->Read("jpg:$ARGV[0]");
my ($image_height, $image_width) = $image->Get('base-rows', 'base-columns');
# Водяной знак
my $watermark = Image::Magick->new;
$watermark->Read("png:$ARGV[1]");
my ($watermark_height, $watermark_width) = $watermark->Get('base-rows', 'base-columns');
# Холст нужен на случай, если знак больше изображения
my $canvas_height = ( $image_height > $watermark_height ? $image_height : $watermark_height );
my $canvas_width = ( $image_width > $watermark_width ? $image_width : $watermark_width );
my $canvas = Image::Magick->new;
$canvas->Set(size => "${canvas_width}x${canvas_height}");
$canvas->Read('NULL:');
my $tiled_layer = Image::Magick->new;
$tiled_layer->Set(size => "${canvas_width}x${canvas_height}");
$tiled_layer->Read('NULL:');
# Размер сетки
my $tile_columns = ceil($image_width / $watermark_width);
my $tile_rows = ceil($image_height / $watermark_height);
# Делаем сетку нечётной, чтобы был центральный элемент
$tile_columns++ if $tile_columns % 2 == 0;
$tile_rows++ if $tile_rows % 2 == 0;
my $center_col = ceil($tile_columns / 2);
my $center_row = ceil($tile_rows / 2);
my $center_x = ($image_width - $watermark_width) * 0.5;
my $center_y = ($image_height - $watermark_height) * 0.5;
for my $col (1 .. $tile_columns) {
for my $row (1 .. $tile_rows) {
my $x = $center_x + ($col - $center_col) * $watermark_width;
my $y = $center_y + ($row - $center_row) * $watermark_height;
$tiled_layer->Composite(
image => $watermark,
compose => 'over',
x => $x,
y => $y,
gravity => 'NorthWest',
);
}
}
$canvas->Composite(
image => $image,
compose => 'over',
gravity => 'center',
);
$canvas->Composite(
image => $tiled_layer,
compose => 'over',
);
$canvas->Crop(
x => ($canvas_width - $image_width) * 0.5,
y => ($canvas_height - $image_height) * 0.5,
width => $image_width,
height => $image_height,
);
$canvas->Set(quality => 88);
$canvas->Write("jpg:$ARGV[2]");3.3. Где здесь ресайз и хранение в S3
В продовом процессе эти скрипты были частью более широкого batch-пайплайна: после чтения исходного файла выполнялся ресайз под нужные размеры каталога, затем накладывался водяной знак, и уже готовые версии отправлялись в AWS S3. То есть задача решалась не как "один эффект", а как потоковая подготовка ассетов для магазина.
Ниже - упрощённый пример запуска из консоли (показан для понимания процесса):
# 1) Сгенерировать PNG-водяной знак для магазина
perl gen-watermark.pl "example-shop.ru" /tmp/example-shop-watermark.png
# 2) Подготовить версию изображения (ресайз делался в пайплайне)
# 3) Наложить водяной знак
perl watermark.pl input.jpg /tmp/example-shop-watermark.png output.jpg
# 4) Далее готовый файл выгружался в AWS S3Сейчас для новых проектов я бы в первую очередь смотрел в сторону Cloudflare Images или аналогичных managed-решений. Но как кейс про кастомную массовую обработку изображений, этот пример всё ещё полезен: особенно когда нужно понять логику, ограничения и компромиссы "под капотом".