Skip to content

fix(gallery): Apply Media Source watermark in ImageService - #730

Merged
biz87 merged 6 commits into
betafrom
fix/issue-729-thumbnail-watermark
Sep 17, 2026
Merged

biz87 merged 6 commits into
betafrom
fix/issue-729-thumbnail-watermark

Conversation

@Ibochkarev

@Ibochkarev Ibochkarev commented Sep 14, 2026

Copy link
Copy Markdown
Member

Описание

В документации галереи описан блок watermark в JSON свойства thumbnails Media Source, но после перехода на Intervention Image ImageService::makeThumbnail() этот ключ не читал. Превью пересоздавались без водяного знака и без записей в error.log.

Теперь overlay накладывается через place() после resize: enabled, path, position, offset_x, offset_y, opacity. Путь резолвится относительно корня сайта (в т.ч. URL-style /assets/...). Файл вне базы или отсутствующий путь пишутся в error.log, превью всё равно создаётся.

position — только имена Intervention (top-leftbottom-right) или tile (мозаика по кадру; тогда offset_x / offset_y — отступы между плитками). Коды phpThumb/MS2 (*, BR, C, …) не принимаются: пишутся в error.log, overlay не ставится. Opacity < 100 печётся в альфа-канал знака (корректный alpha на GD). Effects из docs (sharpen / blur / greyscale / progressive / strip_exif) в этот PR не входят.

Тип изменений

  • Исправление бага (non-breaking change)
  • Новая функциональность (non-breaking change)
  • Breaking change (изменение, ломающее обратную совместимость)
  • Рефакторинг (без изменения функциональности)
  • Документация
  • Другое (опишите):

Связанные Issues

Closes #729

Как это было протестировано?

cd core/components/minishop3
php -l src/Services/ImageService.php
./vendor/bin/phpunit tests/Unit/Services/ImageServiceWatermarkTest.php
# OK (9 tests, 120 assertions)
  • Ручное тестирование
  • Автоматические тесты (composer ci:php / composer test, npm run lint:ci, composer stan / GitHub Actions CI)
  • Тестирование на разных версиях PHP/MODX

Конфигурация тестирования:

  • MiniShop3: ветка fix/issue-729-thumbnail-watermark от beta
  • MODX: —
  • PHP: 8.4

Скриншоты (если применимо)

не приложено

Чеклист

  • Код соответствует стилю проекта
  • Добавлены/обновлены комментарии в сложных местах
  • Изменения не ломают существующую функциональность
  • Лексиконы добавлены на двух языках (ru/en)
  • PHPStan проходит без новых ошибок (composer stan / CI job PHPStan)
  • ESLint проходит без ошибок (npm run lint:ci для Vue)
  • Обновлён CHANGELOG.md (для значимых изменений)

Дополнительные заметки

Пример тайлинга:

"watermark": {
  "enabled": true,
  "path": "assets/images/watermark.png",
  "position": "tile",
  "offset_x": 20,
  "offset_y": 20,
  "opacity": 35
}

Документация на docs.modx.pro должна описывать те же значения position (Intervention + tile), без * / wmi-кодов.

@biz87

biz87 commented Sep 16, 2026

Copy link
Copy Markdown
Member

Проверил на dev (PHP 8.3, драйвер GD, Intervention Image 3.11.7), ImageService из aca64dc9, слитого с актуальной beta. Сравнивал пиксели готовых превью:

  • без блока watermark превью побайтно совпадает с превью из beta;
  • на непрозрачном исходнике center и * с opacity: 50 дают правильное смешивание, у всех плиток одинаковая сила; отступы между плитками не закрашиваются;
  • работают относительный путь, /assets/... и абсолютный путь внутри корня сайта; ../ за пределы корня пишет ошибку в журнал, а превью всё равно создаётся;
  • повторное использование одного Image при тайлинге (aca64dc9) — верно, Imagick PlaceModifier меняет альфа-канал самого водяного знака.

Возвращаю из-за одного артефакта на GD.

GD: серые плашки вокруг знака на прозрачном исходнике

При opacity < 100 Intervention на GD накладывает знак через imagecopymerge(), а она не учитывает альфа-канал. Если у исходника прозрачный фон (PNG/WebP-вырезка товара), вся рамка водяного знака становится непрозрачной: прозрачная часть логотипа закрашивается серым (127,127,127). Об этом ограничении сказано в комментарии к placeTransparent() в самом Intervention.

Как проверял: исходник 400×300 с прозрачным фоном и синим товаром в центре, логотип 120×40 с прозрачным фоном, opacity: 50, форматы jpg, webp и png — во всех трёх один и тот же результат:

прозрачная часть логотипа фон превью
bottom-right, opacity: 100 не изменилась не изменился
bottom-right, opacity: 50 серая непрозрачная плашка 120×40 не изменился
*, opacity: 50 серая сетка по всему кадру закрыт плитками

Пример из документации — как раз opacity: 50. GD — единственный драйвер там, где нет Imagick.

Как обойти в ImageService: применить прозрачность к водяному знаку один раз (умножить его альфа-канал на opacity / 100) и вызывать place() с opacity = 100. Тогда Intervention использует imagecopy() с альфа-смешиванием, а оно прозрачность учитывает. Проверил это прототипом на GD:

  • на прозрачном исходнике прозрачная часть логотипа остаётся прозрачной (a=127), полоса логотипа полупрозрачна (a=63);
  • на сером исходнике результат совпадает с нынешним (192,63,63);
  • знак не меняется при наложении, так что один декодированный Image можно переиспользовать для всех плиток. 19 200 плиток 8×8 на 1600×1200: 0,39 с против 4,4 с в aca64dc9, где файл читается с диска на каждую плитку.

Для Imagick поведение Intervention корректное, там достаточно не переиспользовать объект (как сейчас) или применить прозрачность один раз тем же способом.

В тест стоит добавить сравнение пикселей на прозрачном исходнике: сейчас тесты сравнивают байты превью целиком, и такой артефакт они не ловят.

Позиции из phpThumb

PR берёт из phpThumb/miniShop2 только *, а остальные коды выравнивания wmi (TL, T, TR, L, C, R, BL, B, BR) Intervention не знает. Он молча ставит знак в левый верхний угол. Проверил: "position": "BR" — знак в левом верхнем углу, в журнале ничего. Получается наполовину поддержанное наследие: * работает, BR — нет, и ошибку не видно.

Стоит решить в этом PR одним из двух способов:

  1. Карта соответствий. Сопоставить все коды phpThumb с позициями Intervention (BRbottom-right, Ccenter, * → тайлинг и т. д.).
  2. Убрать наследие phpThumb совсем. Принимать только имена Intervention и tile, убрать * и отсылки к phpThumb/MS2 из кода и описания PR, в документации описывать только эти значения. Формат thumbnails в MS3 и так несовместим с MS2 (width/height/watermark вместо w/h/fltr), настройки из MS2 всё равно переписываются вручную.

В обоих случаях неизвестное значение position стоит писать в журнал, а не молча ставить знак в угол.

@Ibochkarev

Copy link
Copy Markdown
Member Author

@biz87 Спасибо за ревью — учёл в d363963.

GD + прозрачный исходник: opacity печётся в альфа-канал водяного знака один раз, place() всегда с opacity = 100 → на GD идёт imagecopy(), без серых плашек. Тот же Image переиспользуется для всех плиток (без чтения файла на каждую).

Позиции: карта phpThumb TL…BR / C / * → Intervention + tile. Неизвестное значение пишется в error.log, overlay не ставится (не silent top-left).

Тесты: прозрачный исходник + полупрозрачный знак с clear-половиной; BRbottom-right; unknown position → log + байты как без watermark.
./vendor/bin/phpunit tests/Unit/Services/ImageServiceWatermarkTest.php → OK (9 tests, 120 assertions).

@Ibochkarev

Copy link
Copy Markdown
Member Author

@biz87 По позициям взяли второй вариант: наследие phpThumb/MS2 убрано.

  • Принимаются только Intervention (top-leftbottom-right) и tile.
  • *, BR, C и прочие wmi-коды → error.log, overlay не ставится.
  • Описание PR обновлено; в docs.modx.pro нужно синхронно убрать * / wmi из раздела Watermarks.

Коммит: 72a30e53.

Docs describe watermark in thumbnails JSON, but makeThumbnail ignored
the key after the phpThumb → Intervention migration. Place the overlay
after resize, log missing/invalid paths, and keep generation succeeding.
Intelephense expected Intervention\Image\Interfaces\EncoderInterface;
the @return pointed at a non-existent Encoders\EncoderInterface.
Match wmi alignment *: cover the canvas with repeated marks.
offset_x/offset_y act as inter-tile margins; alias tile accepted.
Imagick PlaceModifier divides alpha on the same object each call when
opacity is below 100, so only the first tile stayed visible. Pass the
file path into every place() and assert a far-corner pixel differs.
GD place() with opacity < 100 used imagecopymerge and painted opaque
boxes on transparent sources; bake alpha once and place at 100. Map
phpThumb TL…BR codes, log unknown positions, reuse one mark for tiles.
Accept only Intervention place names and tile; reject *, BR, and other
wmi codes with an error log. Align PR/docs wording with MS3 thumbnails.
@Ibochkarev
Ibochkarev force-pushed the fix/issue-729-thumbnail-watermark branch from 72a30e5 to 2f65eec Compare September 16, 2026 15:23
@biz87
biz87 merged commit 1d8938e into beta Sep 17, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Watermark в thumbnails Media Source игнорируется (docs ≠ код)

2 participants