Перейти к содержимому
Поиск
Php Разработка

PHP: полный мануал с примерами, функциями и ООП

Практический мануал PHP: от переменных и читаемого кода до безопасных форм, интеграций, базы данных и ООП на примере магазина. Учебный маршрут, поиск функций, подробные примеры, разбор ошибок и исходники для скачивания.

Александр НекрасовИзменено:
Практический мануал PHP: от переменных и функций к объектной модели интернет-магазина
PHP 8.5.11468 глав6256 карточек справочника
Дерево руководства

Глава 393 · После объектов, пикселей и глубины каналов

Imagick: скрыть графическую метку и извлечь её из PNG16

Настоящий носитель, извлечённая метка, понятные offset и размеры; сохранение Q16/PNG16, ошибки PNG8/JPEG и воспроизводимый архив с объяснением границ метода.

Скрыть метку, сохранить PNG16 и увидеть настоящее извлечение

В учебном архиве есть изображение-носитель и маленькая чёрно-белая метка. Хотим сохранить метку внутри пикселей, затем открыть готовый файл и увидеть её снова. Ниже настоящий результат PHP: привычный вид картинки почти не меняется, но извлечённый рисунок появляется отдельным файлом. Это полезный опыт для понимания скрытых данных и причин их потери при обработке; защитой от копирования или цифровой подписью он не является.

Исходный носитель, его вид после скрытия и действительно извлечённая метка 40×32; PHP увеличил просмотр метки в 10 раз
Исходный носитель, его вид после скрытия и действительно извлечённая метка 40×32; PHP увеличил просмотр метки в 10 раз

Оба собственных рисунка приложены в формате base64. Полный example.php восстановит carrier.png размером 400×320 и mark.png размером 40×32. Короткая запись ниже предполагает, что эти файлы уже подготовлены. Выражение new Imagick читает изображение; стрелка -> вызывает метод; в скобках идут объект метки и целое число 17. Знак = сохраняет новый объект результата. Число 17 — начальное место записи в последовательности пикселей, а не секретный ключ.

$carrier = new Imagick(__DIR__ . "/carrier.png");
$mark = new Imagick(__DIR__ . "/mark.png");
$encoded = $carrier->steganoImage($mark, 17);

Получили объект в памяти; теперь его нужно сохранить с достаточной глубиной каналов. Для проверенной Q16-сборки используем PNG16. В полной программе sgSavePNG выполняет следующие настройки на копии и сразу открывает файл для проверки:

$encoded->setImageFormat("png");
$encoded->setImageDepth(16);
$encoded->setOption("png:bit-depth", "16");
$encoded->setOption("png:color-type", "2");
$encoded->writeImage(__DIR__ . "/encoded16.png");

Значение 2 у png:color-type означает три RGB-канала. Нам нужно сохранить их младшие биты. На странице показаны отдельные 8-битные копии для удобного просмотра; файл с данными сохраняется отдельно. Понятия объекта, аргумента и результата и глубины каналов разобраны в связанных главах.

Скрыть собственную метку и действительно извлечь её из PNG16

Сохранён настоящий PNG16-носитель и извлечена исходная метка 40×32 без различий RGB. В носителе изменились 9327 значений каналов максимум на 1 из 65535. На странице показана PNG8-копия для просмотра; извлечение выполняется из другого файла first-data16.png.

Скрыть собственную метку и действительно извлечь её из PNG16Пример для указанного окружения
<?php
declare(strict_types=1);
require __DIR__.'/stegano-fixture.php';
sgRestore();
$carrier=new Imagick(__DIR__.'/carrier.png');
$mark=new Imagick(__DIR__.'/mark.png');
$beforeCarrier=sgState($carrier);$beforeMark=sgState($mark);
$encoded=$carrier->steganoImage($mark,17);
$saved=sgSavePNG($encoded,'first-data16.png',16);
$decoded=new Imagick();$decoded->setSizeOffset(40,32,17);
$decoded->readImage('STEGANO:'.__DIR__.'/first-data16.png');
$recovered=sgSavePNG($decoded,'first-recovered.png',8);
$view=sgMarkView($decoded);
$images=['carrier'=>sgImage($carrier,'first-carrier8.png'),'stegano'=>sgImage($encoded,'first-stegano8.png'),'recovered'=>sgImage($view,'first-recovered-view8.png')];
$rects=sgCompare([$carrier,$encoded,$view],'stegano-first.png');$difference=sgDifference($carrier,$encoded);
sgDiagnostic('stegano-first',['executed_api'=>'steganoImage','returned_type'=>get_debug_type($encoded),'distinct_object'=>$encoded!==$carrier&&$encoded!==$mark,
    'before_carrier'=>$beforeCarrier,'before_mark'=>$beforeMark,'after_carrier'=>sgState($carrier),'after_mark'=>sgState($mark),'encoded'=>sgState($encoded),'decoded'=>sgState($decoded),'difference'=>$difference,'saved'=>$saved,'recovered'=>$recovered,'offset'=>17,
    'images'=>$images,'panel_keys'=>['carrier','stegano','recovered'],'panel_geometry'=>$rects,'source_unchanged'=>sgState($carrier)===$beforeCarrier&&sgState($mark)===$beforeMark,
    'checks'=>['new_Imagick'=>$encoded instanceof Imagick&&$encoded!==$carrier&&$encoded!==$mark,'PNG16_RGB_exact'=>$saved['state']['RGB16_sha256']===sgHash($encoded),'mark_recovered_exact'=>sgHash($decoded)===sgHash($mark),'changed_lowbits'=>$difference['changed_channels']===9327&&$difference['max_quantum_delta']===1,'source_RGB_geometry_preserved'=>sgState($carrier)===$beforeCarrier&&sgState($mark)===$beforeMark]]);
echo "stegano-first.png\nfirst-carrier8.png\nfirst-stegano8.png\nfirst-recovered-view8.png\nfirst-data16.png\nfirst-recovered.png\nmetrics.json\nreturn=Imagick recovered-exact=1 changed-channels=9327 max-delta=1 source-RGB-unchanged=1\n";

Сохранён настоящий PNG16-носитель и извлечена исходная метка 40×32 без различий RGB. В носителе изменились 9327 значений каналов максимум на 1 из 65535. На странице показана PNG8-копия для просмотра; извлечение выполняется из другого файла first-data16.png. ZIP самодостаточен: example.php, carrier.png.b64, mark.png.b64 и stegano-fixture.php. Распакуйте всё рядом и выполните php example.php. Входы восстанавливаются с проверкой SHA. Вспомогательный PHP сохраняет настоящий PNG16 отдельно от PNG8-превью, читает RGB16 и создаёт поясняющие панели; sgMarkView увеличивает только копию 40×32 в 10 раз через sampleImage. Все изучаемые вызовы steganoImage и чтение STEGANO видны в основном коде. Полный вспомогательный PHP приведён в конце главы. Среда: PHP 8.5.11 / Imagick 3.8.1 / ImageMagick 7.1.1-46 Q16, Windows.

Ожидаемый вывод программы в проверенной среде
stegano-first.png
first-carrier8.png
first-stegano8.png
first-recovered-view8.png
first-data16.png
first-recovered.png
metrics.json
return=Imagick recovered-exact=1 changed-channels=9327 max-delta=1 source-RGB-unchanged=1

Понять младшие биты, начальную позицию и размер скрытой метки

Исходная и восстановленная метка: обе 40×32, для просмотра PHP увеличил копии в 10 раз
Исходная и восстановленная метка: обе 40×32, для просмотра PHP увеличил копии в 10 раз

В 8-битном канале число лежит от 0 до 255, в проверенном 16-битном канале — от 0 до 65535. Младший бит похож на единичный разряд числа: его изменение меняет значение на 1. Метод берёт биты интенсивности метки и распределяет их по младшим разрядам R/G/B носителя. У нашей маленькой метки меняются 9327 значений каналов максимум на 1; это не обязательно 9327 разных пикселей. Объект encoded отличается по числам, даже если экранный PNG8 выглядит как исходник.

Аналогия: в последовательности подписанных ячеек меняем последнюю цифру по известному маршруту. Чтобы прочитать записанное, нужны начальная позиция 17 и исходные размеры метки 40×32. Размер 400×320 относится к контейнеру, а не к извлекаемому рисунку. Это только модель битов; ImageMagick не прячет отдельный PNG-файл в комментарии или EXIF. Восстанавливается интенсивность изображения метки. Для нашего чёрно-белого рисунка получены те же RGB, но полноценное цветное фото метки таким способом не сохраняется.

Называть offset паролем неверно: он задаёт порядок чтения и не шифрует метку. Увеличенные на странице копии нужны, чтобы рассмотреть пиксельный рисунок. Их размеры 400×320 нельзя передавать как размеры скрытой метки: настоящие файлы recovered имеют размер 40×32.

Разобрать четыре строки извлечения и возвращаемый объект

Сохранённый PNG16 снова открыт декодером; настоящая метка 40×32 показана отдельной увеличенной копией
Сохранённый PNG16 снова открыт декодером; настоящая метка 40×32 показана отдельной увеличенной копией
$decoded = new Imagick();
$decoded->setSizeOffset(40, 32, 17);
$decoded->readImage("STEGANO:" . __DIR__ . "/encoded16.png");
$decoded->writeImage(__DIR__ . "/recovered.png");

Вызов new Imagick без имени файла создаёт пустой объект декодера. setSizeOffset сообщает ширину 40, высоту 32 и прежнее смещение 17. Префикс STEGANO: в имени включает специальный декодер при чтении; это не расширение файла и не команда оболочки. readImage читает настоящий сохранённый носитель. После него $decoded содержит метку 40×32, которую writeImage сохраняет отдельно. Эти действия полностью выполняются в PHP, запускать стороннюю команду не нужно.

Передать метку и offset, затем указать размеры для декодера

Исходная и извлечённая метки совпадают. Для чтения понадобились именно 40×32 и offset 17. Сигнатура и Reflection говорят Imagick; фактический результат тоже новый Imagick, а не true.

Передать метку и offset, затем указать размеры для декодераПример для указанного окружения
<?php
declare(strict_types=1);
require __DIR__.'/stegano-fixture.php';
sgRestore();
$carrier=new Imagick(__DIR__.'/carrier.png');
$mark=new Imagick(__DIR__.'/mark.png');
$beforeCarrier=sgState($carrier);$beforeMark=sgState($mark);
$encoded=$carrier->steganoImage($mark,17);
$saved=sgSavePNG($encoded,'minimal-data16.png',16);
$decoded=new Imagick();$decoded->setSizeOffset($mark->getImageWidth(),$mark->getImageHeight(),17);
$decoded->readImage('STEGANO:'.__DIR__.'/minimal-data16.png');
$recovered=sgSavePNG($decoded,'minimal-recovered.png',8);
$markView=sgMarkView($mark);$decodedView=sgMarkView($decoded);
$images=['watermark'=>sgImage($markView,'minimal-watermark-view8.png'),'recovered'=>sgImage($decodedView,'minimal-recovered-view8.png')];
$rects=sgCompare([$markView,$decodedView],'stegano-minimal.png');
$declared=(string)(new ReflectionMethod(Imagick::class,'steganoImage'))->getReturnType();
sgDiagnostic('stegano-minimal',['executed_api'=>'steganoImage','returned_type'=>get_debug_type($encoded),'declared_return'=>$declared,'offset'=>17,'watermark_geometry'=>[40,32],'display_scale'=>10,
    'mark'=>sgState($mark),'decoded'=>sgState($decoded),'encoded'=>sgState($encoded),'saved'=>$saved,'recovered'=>$recovered,'images'=>$images,'panel_keys'=>['watermark','recovered'],'panel_geometry'=>$rects,'source_unchanged'=>sgState($carrier)===$beforeCarrier&&sgState($mark)===$beforeMark,
    'checks'=>['actual_Imagick'=>$encoded instanceof Imagick,'declared_Imagick'=>$declared==='Imagick','right_geometry'=>$decoded->getImageGeometry()===['width'=>40,'height'=>32],'mark_recovered_exact'=>sgHash($decoded)===sgHash($mark),'new_object'=>$encoded!==$carrier&&$encoded!==$mark,'source_RGB_geometry_preserved'=>sgState($carrier)===$beforeCarrier&&sgState($mark)===$beforeMark]]);
echo "stegano-minimal.png\nminimal-watermark-view8.png\nminimal-recovered-view8.png\nminimal-data16.png\nminimal-recovered.png\nmetrics.json\noffset=17 mark=40x32 declared=Imagick actual=Imagick recovered-exact=1\n";

Исходная и извлечённая метки совпадают. Для чтения понадобились именно 40×32 и offset 17. Сигнатура и Reflection говорят Imagick; фактический результат тоже новый Imagick, а не true. ZIP самодостаточен: example.php, carrier.png.b64, mark.png.b64 и stegano-fixture.php. Распакуйте всё рядом и выполните php example.php. Входы восстанавливаются с проверкой SHA. Вспомогательный PHP сохраняет настоящий PNG16 отдельно от PNG8-превью, читает RGB16 и создаёт поясняющие панели; sgMarkView увеличивает только копию 40×32 в 10 раз через sampleImage. Все изучаемые вызовы steganoImage и чтение STEGANO видны в основном коде. Полный вспомогательный PHP приведён в конце главы. Среда: PHP 8.5.11 / Imagick 3.8.1 / ImageMagick 7.1.1-46 Q16, Windows.

Ожидаемый вывод программы в проверенной среде
stegano-minimal.png
minimal-watermark-view8.png
minimal-recovered-view8.png
minimal-data16.png
minimal-recovered.png
metrics.json
offset=17 mark=40x32 declared=Imagick actual=Imagick recovered-exact=1

Вместо чисел 40 и 32 можно передать размеры исходного $mark, пока он доступен. Если извлечение выполняется позже, параметры нужно сохранить отдельно. Официальная сигнатура правильно возвращает Imagick, но строка «true при успехе» в описании результата неточна: у нас новый объект. Не приводите результат к bool и не теряйте его; исходный carrier по RGB останется прежним.

Разобрать три потери данных и сохранить воспроизводимое описание

Три настоящих неудачных восстановления: offset 18, уменьшение PNG до 8 бит и JPEG качества 90
Три настоящих неудачных восстановления: offset 18, уменьшение PNG до 8 бит и JPEG качества 90

Успешное сохранение ещё не означает, что метка пережила обработку. Сначала намеренно прочитаем PNG16 с offset 18 вместо 17. Затем сохраним тот же encoded в PNG8 и JPEG качества 90, извлечём все три варианта и покажем их. Это контролируемые отрицательные результаты: на странице виден настоящий рисунок каждого извлечения.

Показать последствия неверного offset, PNG8 и JPEG

Верное чтение PNG16 совпало с исходной меткой. Offset 18, уменьшение глубины до 8 бит и JPEG качества 90 дали другие данные; реальная картинка каждого извлечения показана рядом. Это три разные причины, а не одна общая ошибка сжатия.

Показать последствия неверного offset, PNG8 и JPEGПример для указанного окружения
<?php
declare(strict_types=1);
require __DIR__.'/stegano-fixture.php';
sgRestore();
$carrier=new Imagick(__DIR__.'/carrier.png');
$mark=new Imagick(__DIR__.'/mark.png');
$beforeCarrier=sgState($carrier);$beforeMark=sgState($mark);
$encoded=$carrier->steganoImage($mark,17);
$data16=sgSavePNG($encoded,'controls-data16.png',16);
$data8=sgSavePNG($encoded,'controls-data8.png',8);
$jpeg=clone $encoded;$jpeg->setImageFormat('jpeg');$jpeg->setImageCompressionQuality(90);$jpeg->writeImage(__DIR__.'/controls-lossy.jpg');
$correct=sgDecode('controls-data16.png',17);
$wrong=sgDecode('controls-data16.png',18);
$from8=sgDecode('controls-data8.png',17);
$fromJpeg=sgDecode('controls-lossy.jpg',17);
$recoveries=['wrong_offset'=>sgState($wrong),'png8'=>sgState($from8),'jpeg'=>sgState($fromJpeg),'correct_png16'=>sgState($correct)];
$raw=['wrong_offset'=>sgSavePNG($wrong,'controls-wrong-recovered.png',8),'png8'=>sgSavePNG($from8,'controls-png8-recovered.png',8),'jpeg'=>sgSavePNG($fromJpeg,'controls-jpeg-recovered.png',8)];
$views=[sgMarkView($wrong),sgMarkView($from8),sgMarkView($fromJpeg)];
$images=['wrong_offset'=>sgImage($views[0],'controls-wrong-offset-view8.png'),'png8'=>sgImage($views[1],'controls-png8-view8.png'),'jpeg'=>sgImage($views[2],'controls-jpeg-view8.png')];
$rects=sgCompare($views,'stegano-controls.png');
sgDiagnostic('stegano-controls',['executed_api'=>'steganoImage','mark'=>sgState($mark),'encoded'=>sgState($encoded),'saved_png16'=>$data16,'saved_png8'=>$data8,'jpeg_quality'=>90,'correct_offset'=>17,'wrong_offset'=>18,'recoveries'=>$recoveries,'raw_recovered'=>$raw,
    'images'=>$images,'panel_keys'=>['wrong_offset','png8','jpeg'],'panel_geometry'=>$rects,'source_unchanged'=>sgState($carrier)===$beforeCarrier&&sgState($mark)===$beforeMark,
    'checks'=>['PNG16_recovery_exact'=>sgHash($correct)===sgHash($mark),'wrong_offset_not_exact'=>sgHash($wrong)!==sgHash($mark),'PNG8_not_exact'=>sgHash($from8)!==sgHash($mark),'JPEG_not_exact'=>sgHash($fromJpeg)!==sgHash($mark),'PNG8_carrier_quantized'=>$data8['state']['RGB16_sha256']!==sgHash($encoded),'source_RGB_geometry_preserved'=>sgState($carrier)===$beforeCarrier&&sgState($mark)===$beforeMark]]);
echo "stegano-controls.png\ncontrols-wrong-offset-view8.png\ncontrols-png8-view8.png\ncontrols-jpeg-view8.png\ncontrols-data16.png\ncontrols-data8.png\ncontrols-lossy.jpg\ncontrols-wrong-recovered.png\ncontrols-png8-recovered.png\ncontrols-jpeg-recovered.png\nmetrics.json\nPNG16=exact offset18=not-exact PNG8=not-exact JPEG90=not-exact\n";

Верное чтение PNG16 совпало с исходной меткой. Offset 18, уменьшение глубины до 8 бит и JPEG качества 90 дали другие данные; реальная картинка каждого извлечения показана рядом. Это три разные причины, а не одна общая ошибка сжатия. ZIP самодостаточен: example.php, carrier.png.b64, mark.png.b64 и stegano-fixture.php. Распакуйте всё рядом и выполните php example.php. Входы восстанавливаются с проверкой SHA. Вспомогательный PHP сохраняет настоящий PNG16 отдельно от PNG8-превью, читает RGB16 и создаёт поясняющие панели; sgMarkView увеличивает только копию 40×32 в 10 раз через sampleImage. Все изучаемые вызовы steganoImage и чтение STEGANO видны в основном коде. Полный вспомогательный PHP приведён в конце главы. Среда: PHP 8.5.11 / Imagick 3.8.1 / ImageMagick 7.1.1-46 Q16, Windows.

Ожидаемый вывод программы в проверенной среде
stegano-controls.png
controls-wrong-offset-view8.png
controls-png8-view8.png
controls-jpeg-view8.png
controls-data16.png
controls-data8.png
controls-lossy.jpg
controls-wrong-recovered.png
controls-png8-recovered.png
controls-jpeg-recovered.png
metrics.json
PNG16=exact offset18=not-exact PNG8=not-exact JPEG90=not-exact
СитуацияПочему это отдельная проблема
PNG16, 40×32, offset 17После полного сохранения и чтения метка совпадает по RGB.
Тот же PNG16, но offset 18Начали чтение не с той позиции; декодер может вернуть изображение без исключения, но с другими данными.
PNG8 после записи в Q16Сначала квантовали 16 бит до 8. Сам PNG сжимает оставшиеся числа без потерь, но потерянные младшие биты не возвращаются.
JPEG качества 90Кодирование меняет пиксели; восстановленная метка не совпала. Повышение качества не является гарантией сохранности этого алгоритма.
PNG8-превью в статьеЕго назначение — показ результата. Файл данных с 16-битными каналами хранится и скачивается отдельно.

Для небольшой архивной задачи удобна функция saveMarkedCopy. Она проверяет условия нашего опыта, возвращает описание с размерами, offset и именем PNG16. Полный пример записывает JSON, действительно читает его снова и использует эти параметры для извлечения. Хеши SHA файла и RGB метки здесь позволяют обнаружить случайную подмену данных относительно сохранённого описания; само открытое описание можно изменить, поэтому оно не является подписью или доказательством авторства.

Сохранить носитель с описанием, прочитать его и разобрать четыре ошибки

Функция создаёт PNG16 и отдельное описание параметров. Описание действительно открывается заново, декодер восстанавливает метку, SHA подтверждает точность файла в опыте. Четыре технические ошибки и отдельный отказ нашей политики сохранены. Открытый JSON с SHA не является цифровой подписью.

Сохранить носитель с описанием, прочитать его и разобрать четыре ошибкиПример для указанного окружения
<?php
declare(strict_types=1);
require __DIR__.'/stegano-fixture.php';
sgRestore();
$carrier=new Imagick(__DIR__.'/carrier.png');
$mark=new Imagick(__DIR__.'/mark.png');
$beforeCarrier=sgState($carrier);$beforeMark=sgState($mark);
function saveMarkedCopy(Imagick $carrier,Imagick $mark,int $offset):array {
    sgValidate($carrier,$mark,$offset);
    $encoded=$carrier->steganoImage($mark,$offset);
    $saved=sgSavePNG($encoded,'delivery-data16.png',16);
    return ['filename'=>$saved['filename'],'width'=>$mark->getImageWidth(),'height'=>$mark->getImageHeight(),'offset'=>$offset,'quantum_bits'=>16,'carrier_sha256'=>$saved['sha256'],'watermark_RGB16_sha256'=>sgHash($mark)];
}
$descriptor=saveMarkedCopy($carrier,$mark,17);
file_put_contents(__DIR__.'/delivery-descriptor.json',json_encode($descriptor,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR)."\n");
$loaded=json_decode((string)file_get_contents(__DIR__.'/delivery-descriptor.json'),true,512,JSON_THROW_ON_ERROR);
$decoded=new Imagick();$decoded->setSizeOffset($loaded['width'],$loaded['height'],$loaded['offset']);
$decoded->readImage('STEGANO:'.__DIR__.'/'.$loaded['filename']);
$recovered=sgSavePNG($decoded,'delivery-recovered.png',8);
$stored=new Imagick(__DIR__.'/'.$loaded['filename']);$view=sgMarkView($decoded);
$images=['carrier'=>sgImage($stored,'workflow-carrier8.png'),'recovered'=>sgImage($view,'workflow-recovered-view8.png')];
$rects=sgCompare([$stored,$view],'stegano-workflow.png');
$errors['missing_size']=sgTry(fn()=>(new Imagick())->readImage('STEGANO:'.__DIR__.'/delivery-data16.png'));
$errors['empty_receiver']=sgTry(fn()=>(new Imagick())->steganoImage($mark,17));
$errors['empty_mark']=sgTry(fn()=>$carrier->steganoImage(new Imagick(),17));
$errors['wrong_type']=sgTry(fn()=>$carrier->steganoImage('mark.png',17));
$policy=sgTry(fn()=>sgValidate($carrier,$mark,-1));
$checks=['descriptor_loaded'=>$loaded===$descriptor,'file_sha_matches'=>hash_file('sha256',__DIR__.'/'.$loaded['filename'])===$loaded['carrier_sha256'],'mark_recovered_exact'=>sgHash($decoded)===$loaded['watermark_RGB16_sha256'],'policy_rejects_negative'=>($policy['error']['class']??null)==='InvalidArgumentException','source_RGB_geometry_preserved'=>sgState($carrier)===$beforeCarrier&&sgState($mark)===$beforeMark];
foreach($errors as$name=>$entry){$checks['caught_'.$name]=isset($entry['error']);}
sgDiagnostic('stegano-workflow',['executed_api'=>'steganoImage','descriptor'=>$descriptor,'stored'=>sgState($stored),'decoded'=>sgState($decoded),'mark'=>sgState($mark),'recovered'=>$recovered,'errors'=>$errors,'policy_error'=>$policy,'images'=>$images,'panel_keys'=>['carrier','recovered'],'panel_geometry'=>$rects,'source_unchanged'=>sgState($carrier)===$beforeCarrier&&sgState($mark)===$beforeMark,'checks'=>$checks]);
echo "stegano-workflow.png\nworkflow-carrier8.png\nworkflow-recovered-view8.png\ndelivery-data16.png\ndelivery-recovered.png\ndelivery-descriptor.json\nmetrics.json\ndescriptor-loaded=1 recovered-exact=1 errors=4 policy-rejection=1 source-RGB-unchanged=1\n";

Функция создаёт PNG16 и отдельное описание параметров. Описание действительно открывается заново, декодер восстанавливает метку, SHA подтверждает точность файла в опыте. Четыре технические ошибки и отдельный отказ нашей политики сохранены. Открытый JSON с SHA не является цифровой подписью. ZIP самодостаточен: example.php, carrier.png.b64, mark.png.b64 и stegano-fixture.php. Распакуйте всё рядом и выполните php example.php. Входы восстанавливаются с проверкой SHA. Вспомогательный PHP сохраняет настоящий PNG16 отдельно от PNG8-превью, читает RGB16 и создаёт поясняющие панели; sgMarkView увеличивает только копию 40×32 в 10 раз через sampleImage. Все изучаемые вызовы steganoImage и чтение STEGANO видны в основном коде. Полный вспомогательный PHP приведён в конце главы. Среда: PHP 8.5.11 / Imagick 3.8.1 / ImageMagick 7.1.1-46 Q16, Windows.

Ожидаемый вывод программы в проверенной среде
stegano-workflow.png
workflow-carrier8.png
workflow-recovered-view8.png
delivery-data16.png
delivery-recovered.png
delivery-descriptor.json
metrics.json
descriptor-loaded=1 recovered-exact=1 errors=4 policy-rejection=1 source-RGB-unchanged=1

Наша sgValidate отдельно требует Q16, неотрицательный offset и небольшую метку без кругового обхода. Это условия воспроизводимого проекта, а не обещание, что библиотека всегда сама проверит эти ограничения. Отказ для offset −1 продемонстрирован нашей функцией; отрицательное значение не передаётся в Imagick. Четыре другие ошибки действительно вызваны соответствующими API: два пустых объекта, строка вместо Imagick и чтение STEGANO без размеров.

Сохранить полный контракт и границы скрытой метки

Официальный контракт и уточнения для работающего кода

Imagick::steganoImage(Imagick $watermark_wand, int $offset): Imagick

Метод скрывает цифровую графическую метку в изображении. Официальное описание говорит об извлечении для подтверждения подлинности и определяет offset как начальную позицию. Сохраняем назначение, но ограничиваем вывод: наличие извлекаемой метки само по себе не является криптографическим подтверждением авторства или неизменности. В алгоритме нет секретного ключа и проверки цифровой подписи. Официальные строки подробностей параметров пусты; ниже они раскрыты на реальных входах. Доступность: PECL imagick 2, PECL imagick 3. Запись не отмечена устаревшей в принятом инвентаре.

Часть вызоваЧто передаётся и получается
$carrierПолучатель: изображение-носитель; в примерах непрозрачный RGB размером 400×320.
$watermark_wandОбязательный объект Imagick с меткой. Здесь это чёрно-белый рисунок 40×32. Алгоритм переносит интенсивность, а не сохраняет все исходные цветовые каналы метки.
$offsetОбязательное целое число: начало обхода пикселей носителя, здесь 17. Это не пароль, не координаты x/y и не размер метки.
РезультатНовый Imagick с изменёнными младшими битами. Сигнатура, файл stub, Reflection и наш запуск согласны; текст Return Values на PHP.net ошибочно говорит true.
ВходыВ опыте RGB и геометрия носителя и метки сохранились. Нельзя обещать неизменность всего объекта: код MagickWand записывает offset в состояние получателя перед созданием результата.
ИзвлечениеНовый объект; setSizeOffset(width, height, offset), затем readImage("STEGANO:путь"). Размеры относятся к метке, а не к носителю.

В официальном XML отдельного раздела ошибок нет. Реальные пустые carrier/mark и чтение STEGANO без размеров дали ImagickException; строка вместо Imagick — TypeError. Эти случаи показаны отдельно от нашего правила проверки offset и объёма. Вызов работает с текущими изображениями объектов; примеры однокадровые. Не считайте его автоматическим обработчиком всех кадров анимации.

Конкретная реализация Q16 использует 16-битные значения каналов. Для маленькой метки в этом опыте меняется только младший бит: 9327 значений максимум на 1 из 65535. Это наблюдение нашего входа, а не обещание невидимости для любой метки и размера. Мы выбираем маленькую метку без обхода носителя по кругу: 17 + 40×32×16 = 20497 меньше 400×320. В точных исходниках кодировщика и декодера граница кругового обхода выражена через ширину в квадрате; пример избегает этой ветки. Это не формула универсальной вместимости.

PNG без потерь сохраняет записанные числа, но предварительное уменьшение глубины с 16 до 8 бит уже изменяет их. Поэтому для Q16 здесь принудительно записывается RGB PNG16 и подтверждается настоящее извлечение. PNG8-копия страницы нужна для просмотра, не для хранения метки. JPEG, изменение размера, обрезка, смена цветовой модели или оптимизатор могут изменить те биты, которые нужны декодеру. Обработанный чужим сайтом файл нельзя считать проверенным носителем по расширению.

Два практических применения ниже: минимальное скрытие и извлечение, затем функция архивной выдачи с параметрами обратного чтения. Если посетитель должен видеть имя автора, нужен видимый водяной знак или композиция. Для проверки неизменности используйте подходящую модель хеширования и доверенное хранение ожидаемого значения; для доказательства происхождения нужна отдельная система подписи. Offset не заменяет ни один из этих механизмов.

Самостоятельный запуск и полезное упражнение

Все четыре ZIP самодостаточны: входы в base64 и вспомогательный PHP включены. Требуются PHP с Imagick, работающие кодировщики и декодировщики PNG/STEGANO/JPEG для всех опытов и право записи в папку. Выполните php example.php. Проверенная среда: PHP 8.5.11 / Imagick 3.8.1 / ImageMagick 7.1.1-46 Q16, Windows. Для другой глубины Q сначала зафиксируйте версию и подтвердите полноценное извлечение; PNG16 здесь проверен именно для указанной сборки.

Упражнение: скачали картинку, которую сайт предварительно уменьшил, и попытались восстановить метку 40×32, но получили мусор. Можно ли «исправить пароль» offset? Ответ: offset не пароль. После изменения пиксельных данных этот маршрут уже ненадёжен; сначала проверьте неизменённый исходный PNG16 с верным описанием. Второе упражнение: PNG8 объявлен форматом «без потерь», но после перехода с 16 до 8 бит метка не совпала. Объяснение: квантование произошло до сжатия PNG.

Чтение и запись больших изображений расходуют память; здесь дополнительно экспортируются все RGB16 ради проверки. Размер метки и сложность битового обхода выбирайте осмысленно; неподдержанную ветку переполнения не используем. Для защиты файлов от подмены или доказательства происхождения выбирают другую систему, для видимого бренда — обычную композицию. Эта глава объясняет скрытие и разрушение метки в пикселях.

Полный вспомогательный PHP из каждого архива
<?php
declare(strict_types=1);
function sgRestore():void {
    $hashes=json_decode('{"carrier.png":"65fc8ba8f32d82b0871c0a000f16c283a58bd915e167c487d47f69a1d5f70d56","mark.png":"74b5be67744f49f4208e6d2b81b337b8e5437e145e376b6e80edd46073fba86c"}',true,512,JSON_THROW_ON_ERROR);
    foreach($hashes as$name=>$hash){$raw=base64_decode((string)file_get_contents(__DIR__.'/'.$name.'.b64'),true);
        if($raw===false||hash('sha256',$raw)!==$hash||file_put_contents(__DIR__.'/'.$name,$raw)===false){throw new RuntimeException('Input missing or changed');}}
}
function sgHash(Imagick $image):string {return hash('sha256',json_encode($image->exportImagePixels(0,0,$image->getImageWidth(),$image->getImageHeight(),'RGB',Imagick::PIXEL_SHORT),JSON_THROW_ON_ERROR));}
function sgState(Imagick $image):array {return ['geometry'=>[$image->getImageWidth(),$image->getImageHeight()],'RGB16_sha256'=>sgHash($image)];}
function sgSavePNG(Imagick $image,string $filename,int $depth):array {
    $c=clone $image;$c->setImageFormat('png');$c->setImageDepth($depth);$c->setOption('png:bit-depth',(string)$depth);$c->setOption('png:color-type','2');$c->setOption('png:exclude-chunk','all');
    if(!$c->writeImage(__DIR__.'/'.$filename)){throw new RuntimeException('Cannot save PNG');}$read=new Imagick(__DIR__.'/'.$filename);
    return ['filename'=>$filename,'sha256'=>hash_file('sha256',__DIR__.'/'.$filename),'bytes'=>filesize(__DIR__.'/'.$filename),'bit_depth'=>$depth,'state'=>sgState($read)];
}
function sgDecode(string $filename,int $offset=17,int $width=40,int $height=32):Imagick {
    $decoded=new Imagick();$decoded->setSizeOffset($width,$height,$offset);$decoded->readImage('STEGANO:'.__DIR__.'/'.$filename);return $decoded;
}
function sgMarkView(Imagick $mark):Imagick {$view=clone $mark;$view->sampleImage(400,320);return $view;}
function sgImage(Imagick $image,string $filename):array {
    $saved=sgSavePNG($image,$filename,8);return sgState($image)+['encoded_png'=>['filename'=>$filename,'RGB16_sha256'=>$saved['state']['RGB16_sha256']],'display_basis'=>'PNG8 display only; mark views enlarged10x by PHP sampleImage, never used as the data carrier'];
}
function sgCompare(array $images,string $filename):array {
    $board=new Imagick();$board->newImage(400*count($images),320,'white','png');$rects=[];
    foreach($images as$i=>$image){$board->compositeImage($image,Imagick::COMPOSITE_COPY,400*$i,0);$rects[]=['x'=>400*$i,'y'=>0,'width'=>400,'height'=>320];}
    sgSavePNG($board,$filename,8);return $rects;
}
function sgDifference(Imagick $a,Imagick $b):array {
    $x=$a->exportImagePixels(0,0,$a->getImageWidth(),$a->getImageHeight(),'RGB',Imagick::PIXEL_SHORT);$y=$b->exportImagePixels(0,0,$b->getImageWidth(),$b->getImageHeight(),'RGB',Imagick::PIXEL_SHORT);$changed=0;$maximum=0;
    foreach($x as$k=>$v){if($v!==$y[$k])$changed++;$maximum=max($maximum,abs($v-$y[$k]));}return ['changed_channels'=>$changed,'max_quantum_delta'=>$maximum];
}
function sgTry(callable $operation):array {try{$r=$operation();return ['type'=>get_debug_type($r),'returned'=>$r instanceof Imagick?sgState($r):$r];}catch(Throwable $e){return ['error'=>['class'=>get_class($e),'message'=>$e->getMessage()]];}}
function sgValidate(Imagick $carrier,Imagick $mark,int $offset):void {
    $bits=Imagick::getQuantumDepth()['quantumDepthLong'];
    if($bits!==16){throw new InvalidArgumentException('This reproducible example requires Q16');}
    if($offset<0||$offset+$mark->getImageWidth()*$mark->getImageHeight()*$bits>min($carrier->getImageWidth()*$carrier->getImageHeight(),$carrier->getImageWidth()**2)){throw new InvalidArgumentException('Project requires a small payload without carrier wrap');}
}
function sgDiagnostic(string $id,array $data):void {
    $doc=['schema'=>'aen-stegano-image-program-diagnostics-v1','example_id'=>$id,'runtime'=>['php'=>PHP_VERSION,'imagick'=>phpversion('imagick'),'imagemagick'=>Imagick::getVersion()]]+$data;
    if(file_put_contents(__DIR__.'/metrics.json',json_encode($doc,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR)."\n")===false){throw new RuntimeException('Cannot write metrics');}
}

sgSavePNG явно разделяет 16-битные данные и 8-битный просмотр; sgDecode передаёт декодеру размеры и offset; sgMarkView увеличивает только просмотрную копию в 10 раз. sgState учитывает RGB и геометрию, не утверждая сохранность всего состояния Imagick. Изучаемые вызовы видны в основном example.php.

Реальные вопросы разработчиков

Эти обсуждения задают практические ситуации. Они не являются нормативным описанием API; решения написаны самостоятельно и проверены в обозначенной среде.

how to restore the watermark set with Imagick::steganoImage(): Официальная страница показывает скрытие, но не объясняет извлечение в PHP. Даём полный прямой путь: новый объект, размеры исходной метки и offset через setSizeOffset, чтение STEGANO: настоящего сохранённого файла.

Imagick::steganoImage usage: Начинающий не может применить метод по короткому описанию без примеров. Начинаем с видимых носителя, метки и результата извлечения, затем разбираем короткий вызов и только после этого отчёт и функцию.

Magick.NET and creating a hidden text watermark: Скрытая метка оказывается заметной или не совпадает после чтения; автор рассчитывает обнаруживать копии на других сайтах. Проверяем глубину сохранённого PNG и точность извлечения отдельно от вида картинки. Не обещаем сохранение метки после обработки сайтом или доказательство авторства.

Adding a Digital Signature to an Image using Golang or ImageMagick: Разработчик называет стеганографию цифровой подписью и получает мусор после извлечения. Разделяем наличие метки и криптографическую подпись. Явно сохраняем размер, offset и неизменённые данные контейнера, проверяем полный путь от записи до обратного чтения.

Cant extract embedded stego message after compression is applied to image?: После JPEG-сжатия исчезает ранее извлекаемое скрытое сообщение. Показываем контролируемый повторный JPEG-запуск и сравнение восстановления; качество JPEG не заменяет сохранение точных младших битов.

Hiding a message in a JPEG image: Рабочий PNG-алгоритм пытаются перенести на JPEG простым изменением формата. Разделяем способ записи младших битов и формат выдачи. PNG16 является проверенным носителем в нашей Q16-сборке; PNG8 и JPEG рассматриваются как отдельные изменённые данные.

Функции и методы этой темы

Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.

Imagick::steganoImageСкрыть графическую метку и извлечь её из настоящего PNG16

PECL imagick 2, PECL imagick 3 · imagick

Imagick::steganoImage(Imagick $watermark_wand, int $offset): Imagick

Официальный контракт и уточнения для работающего кода

Imagick::steganoImage(Imagick $watermark_wand, int $offset): Imagick

Метод скрывает цифровую графическую метку в изображении. Официальное описание говорит об извлечении для подтверждения подлинности и определяет offset как начальную позицию. Сохраняем назначение, но ограничиваем вывод: наличие извлекаемой метки само по себе не является криптографическим подтверждением авторства или неизменности. В алгоритме нет секретного ключа и проверки цифровой подписи. Официальные строки подробностей параметров пусты; ниже они раскрыты на реальных входах. Доступность: PECL imagick 2, PECL imagick 3. Запись не отмечена устаревшей в принятом инвентаре.

Часть вызоваЧто передаётся и получается
$carrierПолучатель: изображение-носитель; в примерах непрозрачный RGB размером 400×320.
$watermark_wandОбязательный объект Imagick с меткой. Здесь это чёрно-белый рисунок 40×32. Алгоритм переносит интенсивность, а не сохраняет все исходные цветовые каналы метки.
$offsetОбязательное целое число: начало обхода пикселей носителя, здесь 17. Это не пароль, не координаты x/y и не размер метки.
РезультатНовый Imagick с изменёнными младшими битами. Сигнатура, файл stub, Reflection и наш запуск согласны; текст Return Values на PHP.net ошибочно говорит true.
ВходыВ опыте RGB и геометрия носителя и метки сохранились. Нельзя обещать неизменность всего объекта: код MagickWand записывает offset в состояние получателя перед созданием результата.
ИзвлечениеНовый объект; setSizeOffset(width, height, offset), затем readImage("STEGANO:путь"). Размеры относятся к метке, а не к носителю.

В официальном XML отдельного раздела ошибок нет. Реальные пустые carrier/mark и чтение STEGANO без размеров дали ImagickException; строка вместо Imagick — TypeError. Эти случаи показаны отдельно от нашего правила проверки offset и объёма. Вызов работает с текущими изображениями объектов; примеры однокадровые. Не считайте его автоматическим обработчиком всех кадров анимации.

Конкретная реализация Q16 использует 16-битные значения каналов. Для маленькой метки в этом опыте меняется только младший бит: 9327 значений максимум на 1 из 65535. Это наблюдение нашего входа, а не обещание невидимости для любой метки и размера. Мы выбираем маленькую метку без обхода носителя по кругу: 17 + 40×32×16 = 20497 меньше 400×320. В точных исходниках кодировщика и декодера граница кругового обхода выражена через ширину в квадрате; пример избегает этой ветки. Это не формула универсальной вместимости.

PNG без потерь сохраняет записанные числа, но предварительное уменьшение глубины с 16 до 8 бит уже изменяет их. Поэтому для Q16 здесь принудительно записывается RGB PNG16 и подтверждается настоящее извлечение. PNG8-копия страницы нужна для просмотра, не для хранения метки. JPEG, изменение размера, обрезка, смена цветовой модели или оптимизатор могут изменить те биты, которые нужны декодеру. Обработанный чужим сайтом файл нельзя считать проверенным носителем по расширению.

Два практических применения ниже: минимальное скрытие и извлечение, затем функция архивной выдачи с параметрами обратного чтения. Если посетитель должен видеть имя автора, нужен видимый водяной знак или композиция. Для проверки неизменности используйте подходящую модель хеширования и доверенное хранение ожидаемого значения; для доказательства происхождения нужна отдельная система подписи. Offset не заменяет ни один из этих механизмов.

Передать метку и offset, затем указать размеры для декодера

Исходная и извлечённая метки совпадают. Для чтения понадобились именно 40×32 и offset 17. Сигнатура и Reflection говорят Imagick; фактический результат тоже новый Imagick, а не true.

Передать метку и offset, затем указать размеры для декодераПример для указанного окружения
<?php
declare(strict_types=1);
require __DIR__.'/stegano-fixture.php';
sgRestore();
$carrier=new Imagick(__DIR__.'/carrier.png');
$mark=new Imagick(__DIR__.'/mark.png');
$beforeCarrier=sgState($carrier);$beforeMark=sgState($mark);
$encoded=$carrier->steganoImage($mark,17);
$saved=sgSavePNG($encoded,'minimal-data16.png',16);
$decoded=new Imagick();$decoded->setSizeOffset($mark->getImageWidth(),$mark->getImageHeight(),17);
$decoded->readImage('STEGANO:'.__DIR__.'/minimal-data16.png');
$recovered=sgSavePNG($decoded,'minimal-recovered.png',8);
$markView=sgMarkView($mark);$decodedView=sgMarkView($decoded);
$images=['watermark'=>sgImage($markView,'minimal-watermark-view8.png'),'recovered'=>sgImage($decodedView,'minimal-recovered-view8.png')];
$rects=sgCompare([$markView,$decodedView],'stegano-minimal.png');
$declared=(string)(new ReflectionMethod(Imagick::class,'steganoImage'))->getReturnType();
sgDiagnostic('stegano-minimal',['executed_api'=>'steganoImage','returned_type'=>get_debug_type($encoded),'declared_return'=>$declared,'offset'=>17,'watermark_geometry'=>[40,32],'display_scale'=>10,
    'mark'=>sgState($mark),'decoded'=>sgState($decoded),'encoded'=>sgState($encoded),'saved'=>$saved,'recovered'=>$recovered,'images'=>$images,'panel_keys'=>['watermark','recovered'],'panel_geometry'=>$rects,'source_unchanged'=>sgState($carrier)===$beforeCarrier&&sgState($mark)===$beforeMark,
    'checks'=>['actual_Imagick'=>$encoded instanceof Imagick,'declared_Imagick'=>$declared==='Imagick','right_geometry'=>$decoded->getImageGeometry()===['width'=>40,'height'=>32],'mark_recovered_exact'=>sgHash($decoded)===sgHash($mark),'new_object'=>$encoded!==$carrier&&$encoded!==$mark,'source_RGB_geometry_preserved'=>sgState($carrier)===$beforeCarrier&&sgState($mark)===$beforeMark]]);
echo "stegano-minimal.png\nminimal-watermark-view8.png\nminimal-recovered-view8.png\nminimal-data16.png\nminimal-recovered.png\nmetrics.json\noffset=17 mark=40x32 declared=Imagick actual=Imagick recovered-exact=1\n";

Исходная и извлечённая метки совпадают. Для чтения понадобились именно 40×32 и offset 17. Сигнатура и Reflection говорят Imagick; фактический результат тоже новый Imagick, а не true. ZIP самодостаточен: example.php, carrier.png.b64, mark.png.b64 и stegano-fixture.php. Распакуйте всё рядом и выполните php example.php. Входы восстанавливаются с проверкой SHA. Вспомогательный PHP сохраняет настоящий PNG16 отдельно от PNG8-превью, читает RGB16 и создаёт поясняющие панели; sgMarkView увеличивает только копию 40×32 в 10 раз через sampleImage. Все изучаемые вызовы steganoImage и чтение STEGANO видны в основном коде. Полный вспомогательный PHP приведён в конце главы. Среда: PHP 8.5.11 / Imagick 3.8.1 / ImageMagick 7.1.1-46 Q16, Windows.

Ожидаемый вывод программы в проверенной среде
stegano-minimal.png
minimal-watermark-view8.png
minimal-recovered-view8.png
minimal-data16.png
minimal-recovered.png
metrics.json
offset=17 mark=40x32 declared=Imagick actual=Imagick recovered-exact=1

Сохранить носитель с описанием, прочитать его и разобрать четыре ошибки

Функция создаёт PNG16 и отдельное описание параметров. Описание действительно открывается заново, декодер восстанавливает метку, SHA подтверждает точность файла в опыте. Четыре технические ошибки и отдельный отказ нашей политики сохранены. Открытый JSON с SHA не является цифровой подписью.

Сохранить носитель с описанием, прочитать его и разобрать четыре ошибкиПример для указанного окружения
<?php
declare(strict_types=1);
require __DIR__.'/stegano-fixture.php';
sgRestore();
$carrier=new Imagick(__DIR__.'/carrier.png');
$mark=new Imagick(__DIR__.'/mark.png');
$beforeCarrier=sgState($carrier);$beforeMark=sgState($mark);
function saveMarkedCopy(Imagick $carrier,Imagick $mark,int $offset):array {
    sgValidate($carrier,$mark,$offset);
    $encoded=$carrier->steganoImage($mark,$offset);
    $saved=sgSavePNG($encoded,'delivery-data16.png',16);
    return ['filename'=>$saved['filename'],'width'=>$mark->getImageWidth(),'height'=>$mark->getImageHeight(),'offset'=>$offset,'quantum_bits'=>16,'carrier_sha256'=>$saved['sha256'],'watermark_RGB16_sha256'=>sgHash($mark)];
}
$descriptor=saveMarkedCopy($carrier,$mark,17);
file_put_contents(__DIR__.'/delivery-descriptor.json',json_encode($descriptor,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR)."\n");
$loaded=json_decode((string)file_get_contents(__DIR__.'/delivery-descriptor.json'),true,512,JSON_THROW_ON_ERROR);
$decoded=new Imagick();$decoded->setSizeOffset($loaded['width'],$loaded['height'],$loaded['offset']);
$decoded->readImage('STEGANO:'.__DIR__.'/'.$loaded['filename']);
$recovered=sgSavePNG($decoded,'delivery-recovered.png',8);
$stored=new Imagick(__DIR__.'/'.$loaded['filename']);$view=sgMarkView($decoded);
$images=['carrier'=>sgImage($stored,'workflow-carrier8.png'),'recovered'=>sgImage($view,'workflow-recovered-view8.png')];
$rects=sgCompare([$stored,$view],'stegano-workflow.png');
$errors['missing_size']=sgTry(fn()=>(new Imagick())->readImage('STEGANO:'.__DIR__.'/delivery-data16.png'));
$errors['empty_receiver']=sgTry(fn()=>(new Imagick())->steganoImage($mark,17));
$errors['empty_mark']=sgTry(fn()=>$carrier->steganoImage(new Imagick(),17));
$errors['wrong_type']=sgTry(fn()=>$carrier->steganoImage('mark.png',17));
$policy=sgTry(fn()=>sgValidate($carrier,$mark,-1));
$checks=['descriptor_loaded'=>$loaded===$descriptor,'file_sha_matches'=>hash_file('sha256',__DIR__.'/'.$loaded['filename'])===$loaded['carrier_sha256'],'mark_recovered_exact'=>sgHash($decoded)===$loaded['watermark_RGB16_sha256'],'policy_rejects_negative'=>($policy['error']['class']??null)==='InvalidArgumentException','source_RGB_geometry_preserved'=>sgState($carrier)===$beforeCarrier&&sgState($mark)===$beforeMark];
foreach($errors as$name=>$entry){$checks['caught_'.$name]=isset($entry['error']);}
sgDiagnostic('stegano-workflow',['executed_api'=>'steganoImage','descriptor'=>$descriptor,'stored'=>sgState($stored),'decoded'=>sgState($decoded),'mark'=>sgState($mark),'recovered'=>$recovered,'errors'=>$errors,'policy_error'=>$policy,'images'=>$images,'panel_keys'=>['carrier','recovered'],'panel_geometry'=>$rects,'source_unchanged'=>sgState($carrier)===$beforeCarrier&&sgState($mark)===$beforeMark,'checks'=>$checks]);
echo "stegano-workflow.png\nworkflow-carrier8.png\nworkflow-recovered-view8.png\ndelivery-data16.png\ndelivery-recovered.png\ndelivery-descriptor.json\nmetrics.json\ndescriptor-loaded=1 recovered-exact=1 errors=4 policy-rejection=1 source-RGB-unchanged=1\n";

Функция создаёт PNG16 и отдельное описание параметров. Описание действительно открывается заново, декодер восстанавливает метку, SHA подтверждает точность файла в опыте. Четыре технические ошибки и отдельный отказ нашей политики сохранены. Открытый JSON с SHA не является цифровой подписью. ZIP самодостаточен: example.php, carrier.png.b64, mark.png.b64 и stegano-fixture.php. Распакуйте всё рядом и выполните php example.php. Входы восстанавливаются с проверкой SHA. Вспомогательный PHP сохраняет настоящий PNG16 отдельно от PNG8-превью, читает RGB16 и создаёт поясняющие панели; sgMarkView увеличивает только копию 40×32 в 10 раз через sampleImage. Все изучаемые вызовы steganoImage и чтение STEGANO видны в основном коде. Полный вспомогательный PHP приведён в конце главы. Среда: PHP 8.5.11 / Imagick 3.8.1 / ImageMagick 7.1.1-46 Q16, Windows.

Ожидаемый вывод программы в проверенной среде
stegano-workflow.png
workflow-carrier8.png
workflow-recovered-view8.png
delivery-data16.png
delivery-recovered.png
delivery-descriptor.json
metrics.json
descriptor-loaded=1 recovered-exact=1 errors=4 policy-rejection=1 source-RGB-unchanged=1
Источники и обсуждения

Код и руководство с собой

Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.

Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.

Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.