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

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

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

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

Глава 398 · После объектов и базовой работы с изображениями

Imagick: старый mapImage, заданная палитра и прозрачный PNG

Переход на remapImage с явным dithering, четыре разрешённых цвета, сохранение alpha и проверка настоящего экспорта.

Получить постер в заданных цветах после обновления старого кода

В старом обработчике картинок встретился вызов mapImage, а после обновления сервера он больше не работает. Задача при этом осталась полезной: привести постер и наклейку к четырём заданным цветам. Здесь разберём исторический вызов, заменим его явной современной операцией и проверим, что пользователь действительно получит в готовом PNG.

Собственный постер, четыре разрешённых цвета и результат remapImage. Исходная палитра 4×1 показана широкими полосами только для чтения.
Собственный постер, четыре разрешённых цвета и результат remapImage. Исходная палитра 4×1 показана широкими полосами только для чтения.

palette.png — крошечное изображение 4×1, по одному пикселю каждого цвета. Для наглядности оно показано четырьмя полосами 100×320. Оно задаёт набор доступных красок, а не четыре области будущего постера. Размер результата остаётся 400×320. Сам метод не переносит полосы на изображение.

В каждом архиве лежат собственные исходники в base64; example.php восстановит обычные PNG автоматически. Следующий короткий фрагмент можно выполнить после восстановления либо с двумя своими файлами рядом. new Imagick читает файл в объект, -> вызывает его метод, а :: выбирает именованную константу. DITHERMETHOD_NO означает отсутствие dithering — размещения узора соседних цветов для передачи промежуточных оттенков.

$image = new Imagick(__DIR__ . "/poster.png");
$palette = new Imagick(__DIR__ . "/palette.png");
$image->remapImage($palette, Imagick::DITHERMETHOD_NO);
$image->writeImage(__DIR__ . "/poster-limited.png");

Откройте poster-limited.png: плавный фон превратился в цветовые области, и у всего постера осталось четыре разрешённых RGB. Библиотека выбирает близкие цвета из образца. Это не то же самое, что попросить «любые четыре цвета»: для автоматического выбора существует quantizeImage, подробно разобранный в главе о палитрах и таблицах.

Старый mapImage сохранён ниже как исторический контракт. В проверенной сборке он отсутствует, поэтому все запускаемые программы используют remapImage. Отсутствующий метод не подменяется имитацией и не объявляется проверенным.

Заменить старый вызов и получить PNG в четырёх заданных цветах

Собственный постер сохранён как poster-limited.png размером 400×320. После повторного чтения все RGB принадлежат четырём цветам palette.png. Исходный постер и палитра не изменились. Выполнен remapImage; отсутствующий в этой сборке mapImage не вызывался.

Заменить старый вызов и получить PNG в четырёх заданных цветахПример для указанного окружения
<?php
declare(strict_types=1);
require __DIR__ . '/legacy-map-fixture.php';
lmRestore();
$source = new Imagick(__DIR__ . '/poster.png');
$palette = new Imagick(__DIR__ . '/palette.png');
$sticker = new Imagick(__DIR__ . '/sticker.png');
$sourceBefore = lmState($source);
$paletteBefore = lmState($palette);
$stickerBefore = lmState($sticker);
$sourceHash = hash_file('sha256', __DIR__ . '/poster.png');
$image = clone $source;
$ok = $image->remapImage($palette, Imagick::DITHERMETHOD_NO);
$saved = lmSave($image, 'poster-limited.png');
$read = new Imagick(__DIR__ . '/poster-limited.png');
$audit = lmAudit($read, $palette);
$views = [lmView($source),lmPaletteView($palette),lmView($read)];
$images = ['source'=>lmImage($views[0],'first-source8.png'),'palette'=>lmImage($views[1],'first-palette8.png'),'result'=>lmImage($views[2],'first-result8.png')];
$rects = lmCompare($views,'legacy-map-first.png');
lmDiagnostic('legacy-map-first',[
 'set_return'=>$ok,'mapped_state'=>lmState($image),'saved'=>$saved,'readback'=>lmState($read),'audit'=>$audit,'palette_view'=>['input_geometry'=>[4,1],'display_geometry'=>[400,320],'stripe_width'=>100,'order'=>['#182e43','#208494','#e2a040','#f6ecd6']],
 'images'=>$images,'panel_keys'=>['source','palette','result'],'panel_geometry'=>$rects,'source_before'=>$sourceBefore,'source_after'=>lmState($source),'source_sha256'=>$sourceHash,'source_unchanged'=>lmState($source)===$sourceBefore&&hash_file('sha256',__DIR__.'/poster.png')===$sourceHash,'palette_before'=>$paletteBefore,'palette_after'=>lmState($palette),
 'checks'=>['legacy_unavailable'=>!method_exists(Imagick::class,'mapImage'),'current_available'=>method_exists(Imagick::class,'remapImage'),'return_true'=>$ok===true,'only_reference_rgb'=>$audit['all_rgb_in_palette'],'four_colors'=>$audit['rgb_color_count']===4,'palette_unchanged'=>lmState($palette)===$paletteBefore,'geometry_preserved'=>$read->getImageGeometry()===['width'=>400,'height'=>320],'saved_pixels_equal'=>lmHash($image)===lmHash($read)]
]);
echo "legacy-map-first.png\nfirst-source8.png\nfirst-palette8.png\nfirst-result8.png\nposter-limited.png\nmetrics.json\nlegacy=unavailable current=remapImage return=true colors=4 palette-membership=passed\n";

Собственный постер сохранён как poster-limited.png размером 400×320. После повторного чтения все RGB принадлежат четырём цветам palette.png. Исходный постер и палитра не изменились. Выполнен remapImage; отсутствующий в этой сборке mapImage не вызывался. В архиве example.php, три файла .png.b64 и legacy-map-fixture.php. Распакуйте всё рядом и выполните php example.php: обычные poster.png, palette.png и sticker.png восстановятся автоматически. Полный helper приведён в конце главы. Проверено в PHP 8.5.11 / Imagick 3.8.1 / ImageMagick 7.1.1-46 Q16, Windows. Реально выполнен современный remapImage; исторический mapImage здесь недоступен и не запускался.

Ожидаемый вывод после настоящего запуска
legacy-map-first.png
first-source8.png
first-palette8.png
first-result8.png
poster-limited.png
metrics.json
legacy=unavailable current=remapImage return=true colors=4 palette-membership=passed

Перенести bool-переключатель в явную политику dithering

Одинаковые исходник и палитра: без dithering получаются плоские области, с Floyd–Steinberg — узор из тех же четырёх RGB.
Одинаковые исходник и палитра: без dithering получаются плоские области, с Floyd–Steinberg — узор из тех же четырёх RGB.

Допустим, в прежнем интерфейсе был переключатель «Сглаживать переходы». Он передавал bool — false или true — в mapImage. У remapImage второй аргумент другого вида: целочисленная именованная константа конкретного режима. Поэтому миграция должна явно решить, что делать при каждом положении переключателя.

$method = $enableDither
    ? Imagick::DITHERMETHOD_FLOYDSTEINBERG
    : Imagick::DITHERMETHOD_NO;
$image->remapImage($palette, $method);

Запись условие ? вариант_да : вариант_нет выбирает одно из двух значений. Здесь false означает NO, а true — именно Floyd–Steinberg. Это выбранная политика нашего нового обработчика, а не обещание побитового совпадения с любой старой сборкой mapImage. В полной функции applyPalette clone сохраняет исходник отдельно, а return возвращает обработанный Imagick вызывающему коду.

Dithering не добавляет новые RGB за пределами образца. Он меняет расположение разрешённых цветов, чтобы на расстоянии переход выглядел иначе. Для градиента это может быть полезно; для маленьких букв, чистого логотипа или схемы узор может мешать. Сравнивайте при настоящем размере использования, а не выбирайте режим только по числу цветов.

Явно перевести старый переключатель dithering в выбранную современную политику

Один вызов функции даёт крупные однородные области, другой — узор Floyd–Steinberg. Обе версии используют только четыре разрешённых RGB. Это новая явно выбранная политика, а не обещание повторить все исторические результаты mapImage. Четыре ошибки текущего remapImage перехвачены.

Явно перевести старый переключатель dithering в выбранную современную политикуПример для указанного окружения
<?php
declare(strict_types=1);
require __DIR__ . '/legacy-map-fixture.php';
lmRestore();
$source = new Imagick(__DIR__ . '/poster.png');
$palette = new Imagick(__DIR__ . '/palette.png');
$sticker = new Imagick(__DIR__ . '/sticker.png');
$sourceBefore = lmState($source);
$paletteBefore = lmState($palette);
$stickerBefore = lmState($sticker);
$sourceHash = hash_file('sha256', __DIR__ . '/poster.png');
function applyPalette(Imagick $source, Imagick $palette, bool $enableDither): Imagick
{
    $result = clone $source;
    $method = $enableDither
        ? Imagick::DITHERMETHOD_FLOYDSTEINBERG
        : Imagick::DITHERMETHOD_NO;
    $result->remapImage($palette, $method);
    return $result;
}
$none = applyPalette($source, $palette, false);
$floyd = applyPalette($source, $palette, true);
$reversePalette = clone $palette;
$reversePalette->flopImage();
$reverseResult = applyPalette($source, $reversePalette, false);
$errors = [
 'empty_source'=>lmTry(fn()=>(new Imagick())->remapImage($palette,Imagick::DITHERMETHOD_NO)),
 'empty_palette'=>lmTry(fn()=>$source->remapImage(new Imagick(),Imagick::DITHERMETHOD_NO)),
 'array_palette'=>lmTry(fn()=>$source->remapImage([],Imagick::DITHERMETHOD_NO)),
 'bool_dither'=>lmTry(fn()=>$source->remapImage($palette,true)),
];
$audits = ['none'=>lmAudit($none,$palette),'floyd'=>lmAudit($floyd,$palette)];
$views = [lmView($none),lmView($floyd)];
$images = ['none'=>lmImage($views[0],'migration-none8.png'),'floyd'=>lmImage($views[1],'migration-floyd8.png')];
$rects = lmCompare($views,'legacy-map-migration.png');
lmDiagnostic('legacy-map-migration',[
 'policy'=>['false'=>'DITHERMETHOD_NO','true'=>'DITHERMETHOD_FLOYDSTEINBERG','historical_pixel_equivalence_claimed'=>false],'states'=>['none'=>lmState($none),'floyd'=>lmState($floyd)],'audits'=>$audits,'reverse_palette'=>['state'=>lmState($reversePalette),'result'=>lmState($reverseResult),'same_for_this_fixture'=>lmHash($none)===lmHash($reverseResult)],'errors'=>$errors,'strict_types'=>true,
 'images'=>$images,'panel_keys'=>['none','floyd'],'panel_geometry'=>$rects,'source_before'=>$sourceBefore,'source_after'=>lmState($source),'source_sha256'=>$sourceHash,'source_unchanged'=>lmState($source)===$sourceBefore&&hash_file('sha256',__DIR__.'/poster.png')===$sourceHash,'palette_before'=>$paletteBefore,'palette_after'=>lmState($palette),
 'checks'=>['results_differ'=>lmHash($none)!==lmHash($floyd),'none_reference_rgb'=>$audits['none']['all_rgb_in_palette'],'floyd_reference_rgb'=>$audits['floyd']['all_rgb_in_palette'],'palette_unchanged'=>lmState($palette)===$paletteBefore,'reference_order_control'=>lmHash($none)===lmHash($reverseResult),'empty_source_caught'=>($errors['empty_source']['error']['class']??null)==='ImagickException','empty_palette_caught'=>($errors['empty_palette']['error']['class']??null)==='ImagickException','array_palette_caught'=>($errors['array_palette']['error']['class']??null)==='TypeError','strict_bool_caught'=>($errors['bool_dither']['error']['class']??null)==='TypeError']
]);
echo "legacy-map-migration.png\nmigration-none8.png\nmigration-floyd8.png\nmetrics.json\nfalse=NO true=FLOYDSTEINBERG different=yes both-in-palette=yes strict-errors=4\n";

Один вызов функции даёт крупные однородные области, другой — узор Floyd–Steinberg. Обе версии используют только четыре разрешённых RGB. Это новая явно выбранная политика, а не обещание повторить все исторические результаты mapImage. Четыре ошибки текущего remapImage перехвачены. В архиве example.php, три файла .png.b64 и legacy-map-fixture.php. Распакуйте всё рядом и выполните php example.php: обычные poster.png, palette.png и sticker.png восстановятся автоматически. Полный helper приведён в конце главы. Проверено в PHP 8.5.11 / Imagick 3.8.1 / ImageMagick 7.1.1-46 Q16, Windows. Реально выполнен современный remapImage; исторический mapImage здесь недоступен и не запускался.

Ожидаемый вывод после настоящего запуска
legacy-map-migration.png
migration-none8.png
migration-floyd8.png
metrics.json
false=NO true=FLOYDSTEINBERG different=yes both-in-palette=yes strict-errors=4

В дополнительном контроле мы перевернули четыре пикселя образца слева направо: для этого входа результат без dithering совпал. Это наглядно показывает роль набора цветов, но не гарантирует неизменность при любой палитре, совпадающих расстояниях и версии библиотеки. Порядок индексов в сохранённой таблице PNG/GIF также не обещается. Если старое оборудование требует конкретные номера цветов, нужен отдельный контроль кодера и таблицы; данный экспорт такое требование не решает.

Сохранить прозрачную форму наклейки явно и проверить файл

Исходная наклейка, непрозрачный результат remap и версия с восстановленной alpha. Шахматная подложка принадлежит только просмотру.
Исходная наклейка, непрозрачный результат remap и версия с восстановленной alpha. Шахматная подложка принадлежит только просмотру.

Теперь вместо постера обрабатываем наклейку с прозрачным фоном и полупрозрачной рамкой. Непрозрачная палитра в проверенной операции сделала значения её alpha полностью непрозрачными. При этом getImageAlphaChannel продолжал возвращать true: канал включён, но это ещё не означает, что в нём осталась прозрачность. Важны сами значения, которые видны на результате.

$mapped = clone $sticker;
$mapped->remapImage($palette, Imagick::DITHERMETHOD_NO);
$mapped->setImageAlphaChannel(Imagick::ALPHACHANNEL_SET);
$mapped->compositeImage($sticker, Imagick::COMPOSITE_COPYALPHA, 0, 0);

Здесь remap выбирает новые RGB, а COPYALPHA затем копирует исходный канал прозрачности из $sticker в ту же геометрию. 0, 0 — положение начала исходника. Мы заранее знаем, что размеры обоих изображений одинаковы: 400×320. Возвращается исходная alpha, а не исходные RGB. Это явная политика «меняем краски, сохраняем силуэт и полупрозрачность», которая подходит данной наклейке.

Сохранить настоящую прозрачность наклейки при замене RGB

На этом входе remapImage с непрозрачной палитрой сделал все значения alpha непрозрачными, хотя флаг канала остался true. COPYALPHA вернул исходные значения; они совпали и после записи sticker-restored.png. RGB остались выбранными цветами палитры. Клетки видны только в иллюстрациях, в PNG их нет.

Сохранить настоящую прозрачность наклейки при замене RGBПример для указанного окружения
<?php
declare(strict_types=1);
require __DIR__ . '/legacy-map-fixture.php';
lmRestore();
$source = new Imagick(__DIR__ . '/poster.png');
$palette = new Imagick(__DIR__ . '/palette.png');
$sticker = new Imagick(__DIR__ . '/sticker.png');
$sourceBefore = lmState($source);
$paletteBefore = lmState($palette);
$stickerBefore = lmState($sticker);
$sourceHash = hash_file('sha256', __DIR__ . '/poster.png');
$mapped = clone $sticker;
$mapped->remapImage($palette, Imagick::DITHERMETHOD_NO);
$mappedState = lmState($mapped);
$restored = clone $mapped;
$restored->setImageAlphaChannel(Imagick::ALPHACHANNEL_SET);
$restored->compositeImage($sticker, Imagick::COMPOSITE_COPYALPHA, 0, 0);
$saved = lmSave($restored, 'sticker-restored.png', 6);
$read = new Imagick(__DIR__ . '/sticker-restored.png');
$audit = lmAudit($read,$palette);
$views = [lmView($sticker),lmView($mapped),lmView($read)];
$images = ['source'=>lmImage($views[0],'alpha-source8.png'),'mapped'=>lmImage($views[1],'alpha-mapped8.png'),'restored'=>lmImage($views[2],'alpha-restored8.png')];
$rects = lmCompare($views,'legacy-map-alpha.png');
lmDiagnostic('legacy-map-alpha',[
 'sticker_before'=>$stickerBefore,'sticker_after'=>lmState($sticker),'mapped_state'=>$mappedState,'restored_state'=>lmState($restored),'saved'=>$saved,'readback'=>lmState($read),'audit'=>$audit,'display'=>['checkerboard_cell'=>20,'checkerboard_is_output_alpha'=>false],
 'images'=>$images,'panel_keys'=>['source','mapped','restored'],'panel_geometry'=>$rects,'source_before'=>$sourceBefore,'source_after'=>lmState($source),'source_sha256'=>$sourceHash,'source_unchanged'=>lmState($source)===$sourceBefore&&hash_file('sha256',__DIR__.'/poster.png')===$sourceHash,'palette_before'=>$paletteBefore,'palette_after'=>lmState($palette),
 'checks'=>['input_has_transparency'=>$stickerBefore['alpha_range'][0]===0,'mapped_alpha_flag_true'=>$mappedState['alpha_enabled']===true,'mapped_values_opaque'=>$mappedState['alpha_range']===[65535,65535],'source_alpha_restored'=>lmHash($restored,'A')===lmHash($sticker,'A'),'saved_alpha_restored'=>lmHash($read,'A')===lmHash($sticker,'A'),'RGB_unchanged_by_alpha_copy'=>lmHash($restored,'RGB')===lmHash($mapped,'RGB'),'saved_reference_rgb'=>$audit['all_rgb_in_palette'],'sticker_unchanged'=>lmState($sticker)===$stickerBefore,'palette_unchanged'=>lmState($palette)===$paletteBefore]
]);
echo "legacy-map-alpha.png\nalpha-source8.png\nalpha-mapped8.png\nalpha-restored8.png\nsticker-restored.png\nmetrics.json\nalpha-flag=true mapped=opaque restored-alpha=exact saved-alpha=exact RGB-in-palette=yes\n";

На этом входе remapImage с непрозрачной палитрой сделал все значения alpha непрозрачными, хотя флаг канала остался true. COPYALPHA вернул исходные значения; они совпали и после записи sticker-restored.png. RGB остались выбранными цветами палитры. Клетки видны только в иллюстрациях, в PNG их нет. В архиве example.php, три файла .png.b64 и legacy-map-fixture.php. Распакуйте всё рядом и выполните php example.php: обычные poster.png, palette.png и sticker.png восстановятся автоматически. Полный helper приведён в конце главы. Проверено в PHP 8.5.11 / Imagick 3.8.1 / ImageMagick 7.1.1-46 Q16, Windows. Реально выполнен современный remapImage; исторический mapImage здесь недоступен и не запускался.

Ожидаемый вывод после настоящего запуска
legacy-map-alpha.png
alpha-source8.png
alpha-mapped8.png
alpha-restored8.png
sticker-restored.png
metrics.json
alpha-flag=true mapped=opaque restored-alpha=exact saved-alpha=exact RGB-in-palette=yes

sticker-restored.png — настоящий PNG с alpha. На странице он показан поверх клеток для удобства; клетки не записаны в этот файл. После наложения полупрозрачного пикселя на фон глаз видит смесь с цветом фона, поэтому видимых оттенков может быть больше четырёх. Проверка относится к хранимым RGB, а не к каждому цвету на скриншоте. Число RGBA-комбинаций тоже отличается от числа уникальных RGB.

Не переносите вывод «remap всегда сохраняет alpha» из одного удачного изображения: наш контроль показывает обратное. Для иной палитры, формата или сборки проверьте собственный результат. Для изучения значений канала и композиции продолжите в главе об alpha и главе о композиции и масках.

Выпустить проверенные PNG и отчёт для каталога

Два действительно записанных файла: постер с Floyd–Steinberg и наклейка без dithering с исходной прозрачностью.
Два действительно записанных файла: постер с Floyd–Steinberg и наклейка без dithering с исходной прозрачностью.

Соберём полезную операцию экспорта. Одному исходнику нужен узор на плавном фоне; другому — чистые края и прозрачность. Функция exportPalettePNG получает изображение, палитру, режим, политику alpha и имя результата. Она обрабатывает копию, сохраняет PNG, читает его новым Imagick и проверяет, что в файле остались только разрешённые RGB.

$poster = exportPalettePNG($source, $palette,
    Imagick::DITHERMETHOD_FLOYDSTEINBERG, false, "poster-web.png");
$stickerResult = exportPalettePNG($sticker, $palette,
    Imagick::DITHERMETHOD_NO, true, "sticker-web.png");

Объявление функции ниже показывает порядок действий полностью. Для прозрачного варианта дополнительно сравнивается прочитанная alpha с исходной. Возвращаемый массив содержит имена, размеры, хеши и проверку палитры; из этих данных создаётся palette-export.json. Таким отчётом удобно связать утверждённую палитру с конкретными выходными файлами.

Экспортировать постер и прозрачную наклейку с проверкой готовых файлов

Готовы poster-web.png, прозрачный sticker-web.png и palette-export.json. Функция прочитала оба записанных файла, проверила принадлежность RGB палитре и сохранность alpha наклейки. JSON связывает палитру и выходные файлы по SHA256; он не является подписью автора.

Экспортировать постер и прозрачную наклейку с проверкой готовых файловПример для указанного окружения
<?php
declare(strict_types=1);
require __DIR__ . '/legacy-map-fixture.php';
lmRestore();
$source = new Imagick(__DIR__ . '/poster.png');
$palette = new Imagick(__DIR__ . '/palette.png');
$sticker = new Imagick(__DIR__ . '/sticker.png');
$sourceBefore = lmState($source);
$paletteBefore = lmState($palette);
$stickerBefore = lmState($sticker);
$sourceHash = hash_file('sha256', __DIR__ . '/poster.png');
function exportPalettePNG(
    Imagick $source,
    Imagick $palette,
    int $ditherMethod,
    bool $preserveAlpha,
    string $filename
): array {
    $result = clone $source;
    $result->remapImage($palette, $ditherMethod);
    if ($preserveAlpha) {
        $result->setImageAlphaChannel(Imagick::ALPHACHANNEL_SET);
        $result->compositeImage($source, Imagick::COMPOSITE_COPYALPHA, 0, 0);
    }
    $saved = lmSave($result, $filename, $preserveAlpha ? 6 : 2);
    $read = new Imagick(__DIR__ . '/' . $filename);
    $audit = lmAudit($read, $palette);
    if (!$audit['all_rgb_in_palette']) {
        throw new RuntimeException('Saved RGB is outside the approved palette');
    }
    if ($preserveAlpha && lmHash($read, 'A') !== lmHash($source, 'A')) {
        throw new RuntimeException('Saved transparency differs from the source');
    }
    return ['saved'=>$saved,'audit'=>$audit,'preserve_alpha'=>$preserveAlpha,
        'alpha_equal'=>lmHash($read,'A')===lmHash($source,'A')];
}
$exports = [
    'poster'=>exportPalettePNG($source,$palette,Imagick::DITHERMETHOD_FLOYDSTEINBERG,false,'poster-web.png'),
    'sticker'=>exportPalettePNG($sticker,$palette,Imagick::DITHERMETHOD_NO,true,'sticker-web.png'),
];
$manifest = [
    'palette_file'=>'palette.png',
    'palette_sha256'=>hash_file('sha256',__DIR__.'/palette.png'),
    'approved_rgb'=>lmRGBSet($palette),
    'exports'=>$exports,
    'note'=>'RGB membership does not promise indexed palette order or only four displayed colors after alpha compositing',
];
file_put_contents(__DIR__ . '/palette-export.json', json_encode(
    $manifest, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR
) . "\n");
$readManifest = json_decode((string)file_get_contents(__DIR__.'/palette-export.json'),true,512,JSON_THROW_ON_ERROR);
$posterRead = new Imagick(__DIR__.'/poster-web.png');
$stickerRead = new Imagick(__DIR__.'/sticker-web.png');
$views = [lmView($posterRead),lmView($stickerRead)];
$images = ['poster'=>lmImage($views[0],'workflow-poster8.png'),'sticker'=>lmImage($views[1],'workflow-sticker8.png')];
$rects = lmCompare($views,'legacy-map-workflow.png');
lmDiagnostic('legacy-map-workflow',[
 'exports'=>$exports,'manifest'=>$readManifest,'manifest_sha256'=>hash_file('sha256',__DIR__.'/palette-export.json'),'sticker_before'=>$stickerBefore,'sticker_after'=>lmState($sticker),'readback'=>['poster'=>lmState($posterRead),'sticker'=>lmState($stickerRead)],
 'images'=>$images,'panel_keys'=>['poster','sticker'],'panel_geometry'=>$rects,'source_before'=>$sourceBefore,'source_after'=>lmState($source),'source_sha256'=>$sourceHash,'source_unchanged'=>lmState($source)===$sourceBefore&&hash_file('sha256',__DIR__.'/poster.png')===$sourceHash,'palette_before'=>$paletteBefore,'palette_after'=>lmState($palette),
 'checks'=>['poster_reference_rgb'=>$exports['poster']['audit']['all_rgb_in_palette'],'sticker_reference_rgb'=>$exports['sticker']['audit']['all_rgb_in_palette'],'sticker_alpha_preserved'=>$exports['sticker']['alpha_equal'],'manifest_bound'=>$readManifest['palette_sha256']===hash_file('sha256',__DIR__.'/palette.png'),'manifest_file_hashes'=>$readManifest['exports']['poster']['saved']['sha256']===hash_file('sha256',__DIR__.'/poster-web.png')&&$readManifest['exports']['sticker']['saved']['sha256']===hash_file('sha256',__DIR__.'/sticker-web.png'),'sticker_unchanged'=>lmState($sticker)===$stickerBefore,'palette_unchanged'=>lmState($palette)===$paletteBefore]
]);
echo "legacy-map-workflow.png\nworkflow-poster8.png\nworkflow-sticker8.png\nposter-web.png\nsticker-web.png\npalette-export.json\nmetrics.json\nexports=2 approved-RGB=yes sticker-alpha=preserved manifest=read-back\n";

Готовы poster-web.png, прозрачный sticker-web.png и palette-export.json. Функция прочитала оба записанных файла, проверила принадлежность RGB палитре и сохранность alpha наклейки. JSON связывает палитру и выходные файлы по SHA256; он не является подписью автора. В архиве example.php, три файла .png.b64 и legacy-map-fixture.php. Распакуйте всё рядом и выполните php example.php: обычные poster.png, palette.png и sticker.png восстановятся автоматически. Полный helper приведён в конце главы. Проверено в PHP 8.5.11 / Imagick 3.8.1 / ImageMagick 7.1.1-46 Q16, Windows. Реально выполнен современный remapImage; исторический mapImage здесь недоступен и не запускался.

Ожидаемый вывод после настоящего запуска
legacy-map-workflow.png
workflow-poster8.png
workflow-sticker8.png
poster-web.png
sticker-web.png
palette-export.json
metrics.json
exports=2 approved-RGB=yes sticker-alpha=preserved manifest=read-back

Откройте poster-web.png и sticker-web.png. Первый непрозрачен, второй можно наложить на фон сайта. Обе картинки используют один заданный набор RGB; это не означает одинаковую частоту цветов. palette-export.json можно прочитать как обычный текст или через JSON-декодирование. SHA256 обнаружит случайную замену палитры или файла, но не подтверждает авторство.

В этом примере PNG записан явно как RGB8 или RGBA8. Наличие четырёх RGB не означает, что файл имеет индексированный формат, фиксированные индексы или минимальный размер. Мы не обещаем уменьшение веса. JPEG после повторного кодирования также может изменить значения цветов; если нужен строгий набор RGB, проверяйте именно итоговый файл. Масштабирование, рисование и композиция после remap способны снова добавить цвета, поэтому окончательную проверку ставят после всей цепочки обработки.

Чтение, перебор цветов, dithering и кодирование требуют времени и памяти, зависящих от размеров изображения и палитры. Наши входы небольшие и собственные; для сервиса нужны допустимые размеры, проверенные пути и ресурсные лимиты. Удаление профилей в helper выбрано для этих синтетических sRGB-иллюстраций; для фотографий с ICC-профилями нужна отдельная цветовая политика. Следующий шаг — чтение и запись изображений или общая палитра нескольких изображений.

Полный контракт mapImage, ошибки и безопасная миграция

Imagick::mapImage: исторический контракт и современный переход

Imagick::mapImage(Imagick $map, bool $dither): bool

mapImage заменяет цвета текущего изображения ближайшими цветами из другого изображения-образца. Метод устарел с Imagick 3.4.4; официальный мануал не рекомендует полагаться на него. Официальная доступность: PECL imagick 2, PECL imagick 3. В нашей сборке Imagick 3.8.1 с ImageMagick 7.1.1-46 method_exists вернул false: этот метод отсутствует. В исходниках 3.8.1 историческая ветка включается только при ImageMagick ниже 7 и если устаревший API не исключён при сборке. Это не означает, что mapImage есть в любой сборке Imagick 3.

АргументИсторическая записьСмысл
mapОбязательный ImagickИзображение-образец с допустимыми цветами. Это объект, а не массив строк, имя файла или карта координат.
ditherОбязательный boolfalse отключает dithering, true разрешает его в историческом API. Узор соседних пикселей передаёт промежуточный вид ограниченным набором цветов; флаг не задаёт современное имя алгоритма.

Официальные текстовые описания обоих параметров на странице mapImage пусты; приведённые пояснения опираются также на закреплённые исходники. Результат bool равен true при успехе. Изображение изменяется в объекте, на котором вызван метод: true не является новым Imagick и не доказывает, что файл уже записан. Официальный раздел ошибок указывает ImagickException. Исторические ветки исходников проверяют наличие изображения и переводят отказ библиотеки в исключение; их выполнение в ImageMagick 6 здесь не заявляется.

// Исторический вызов: не работает в проверенной сборке IM7.
$image->mapImage($palette, false);

// Современный вызов, действительно выполненный в примерах.
$image->remapImage($palette, Imagick::DITHERMETHOD_NO);

Современный remapImage получает Imagick replacement и int из DITHERMETHOD_*, возвращает true при успехе и меняет текущий растр. Официальный контракт этой альтернативы требует ImageMagick 6.4.5 или новее. Перенос не сводится к замене имени: старый bool нельзя оставлять вместо явного режима. В наших программах с declare(strict_types=1) передача true прямо в remapImage дала TypeError. В коде с иными правилами приведения ошибка может проявиться иначе; именованная константа убирает двусмысленность.

Для текущего remapImage реально проверены ImagickException при пустом исходнике и пустом образце, TypeError при массиве вместо Imagick и при bool вместо режима в строгом вызове. Это факты о текущем API; они не выдаются за выполненные тесты старого mapImage. Обработку ошибок следует строить вокруг реальных входов, поддерживаемой сборки и выбранной операции.

Границы: ближайший цвет библиотеки не обязан совпадать с любым художественным или перцептивным критерием; образец должен соответствовать задаче и цветовому пространству. Сопоставление не обещает заданный порядок индексов PNG/GIF, использование каждого цвета или одинаковую частоту цветов. Непрозрачный образец не является обещанием сохранить исходную alpha. Эти условия нужно проверять на настоящем выходе.

Связанные темы: полная карточка remapImage и отличие от quantizeImage/clutImage, палитровые индексы и последовательности, канал alpha и фон, цветовое пространство.

Запуск, упражнения и ответы

Каждый архив содержит example.php, poster.png.b64, palette.png.b64, sticker.png.b64 и legacy-map-fixture.php. Распакуйте всё рядом и выполните php example.php. Исходные PNG восстановятся автоматически; нужны расширение Imagick и доступный remapImage. Проверенная среда: PHP 8.5.11, Imagick 3.8.1, ImageMagick 7.1.1-46 Q16. Четыре программы выполнили современную альтернативу; старый mapImage отсутствует и не выполнялся.

Упражнение: можно ли заменить только mapImage на remapImage и оставить true? Ответ: нужно явно выбрать DITHERMETHOD_*; наш строгий вызов с bool вызвал TypeError. Второе упражнение: alpha-флаг true, но клетки исчезли. Канал сохранён? Он включён, однако все значения могли стать непрозрачными; сравните значения и готовый PNG. Третье упражнение: PNG использует четыре RGB, значит ли это индексированную таблицу в нужном порядке? Нет, формат и индексы — отдельное требование. Четвёртое упражнение: почему после наложения наклейки на белый фон видны дополнительные оттенки? Полупрозрачные пиксели смешиваются с фоном, хотя их хранимые RGB принадлежат палитре.

Полный вспомогательный PHP из каждого архива
<?php
declare(strict_types=1);
function lmRestore(): void {
    $hashes=json_decode('{"poster.png":"257246aa3880f7826416f3dfbdb4f8e22fddf20fd1d820fe19445521b0c57e64","palette.png":"26ce43036ed57d9a6997098786b34f2c528c33227a6f066f07d5f071b8abed4a","sticker.png":"5d82c7c156b2ea47984123cc43536c7c4e1760953cad19d15a78392c25b51cff"}',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');}
    if(!method_exists(Imagick::class,'remapImage'))throw new RuntimeException('This example requires remapImage');
}
function lmHash(Imagick $image,string $map='RGBA'):string {
    return hash('sha256',json_encode($image->exportImagePixels(0,0,$image->getImageWidth(),$image->getImageHeight(),$map,Imagick::PIXEL_SHORT),JSON_THROW_ON_ERROR));
}
function lmState(Imagick $image):array {
    $alpha=$image->exportImagePixels(0,0,$image->getImageWidth(),$image->getImageHeight(),'A',Imagick::PIXEL_SHORT);
    return ['geometry'=>[$image->getImageWidth(),$image->getImageHeight()],'format'=>$image->getImageFormat(),'colorspace'=>$image->getImageColorspace(),'depth'=>$image->getImageDepth(),'RGBA16_sha256'=>lmHash($image),'RGB16_sha256'=>lmHash($image,'RGB'),'alpha16_sha256'=>lmHash($image,'A'),'alpha_enabled'=>$image->getImageAlphaChannel(),'alpha_range'=>[min($alpha),max($alpha)],'rgba_color_count'=>$image->getImageColors()];
}
function lmRGBSet(Imagick $image):array {
    $out=[];
    foreach($image->getImageHistogram()as $pixel){$rgb=$pixel->getColor();$key=sprintf('#%02x%02x%02x',(int)$rgb['r'],(int)$rgb['g'],(int)$rgb['b']);$out[$key]=true;}
    $keys=array_keys($out);sort($keys);return $keys;
}
function lmAudit(Imagick $image,Imagick $palette):array {
    $allowed=lmRGBSet($palette);$actual=lmRGBSet($image);$outside=array_values(array_diff($actual,$allowed));
    return ['allowed_rgb'=>$allowed,'actual_rgb'=>$actual,'outside_palette'=>$outside,'all_rgb_in_palette'=>$outside===[],'rgb_color_count'=>count($actual)];
}
function lmSave(Imagick $image,string $name,int $type=2):array {
    $copy=clone $image;$copy->setImageFormat('png');$copy->setImageDepth(8);$copy->setOption('png:bit-depth','8');$copy->setOption('png:color-type',(string)$type);$copy->setOption('png:exclude-chunk','all');
    if(!$copy->writeImage(__DIR__.'/'.$name))throw new RuntimeException('Cannot save PNG');
    return ['filename'=>$name,'sha256'=>hash_file('sha256',__DIR__.'/'.$name),'bytes'=>filesize(__DIR__.'/'.$name),'png_color_type'=>$type,'state'=>lmState(new Imagick(__DIR__.'/'.$name))];
}
function lmView(Imagick $image):Imagick {
    $view=new Imagick();$view->newImage(400,320,'#f4f5f2','png');$draw=new ImagickDraw();$draw->setStrokeAntialias(false);$draw->setFillColor('#d5deda');
    for($y=0;$y<320;$y+=20)for($x=0;$x<400;$x+=20)if((intdiv($x,20)+intdiv($y,20))%2===0)$draw->rectangle($x,$y,$x+19,$y+19);
    $view->drawImage($draw);$view->compositeImage($image,Imagick::COMPOSITE_OVER,0,0);return $view;
}
function lmPaletteView(Imagick $palette):Imagick {$view=clone $palette;$view->sampleImage(400,320);return $view;}
function lmImage(Imagick $image,string $name):array {$saved=lmSave($image,$name);return ['geometry'=>[$image->getImageWidth(),$image->getImageHeight()],'RGBA16_sha256'=>lmHash($image),'encoded_png'=>['filename'=>$name,'RGBA16_sha256'=>$saved['state']['RGBA16_sha256']],'display_basis'=>'Actual PHP output; checkerboard where present belongs only to preview, not RGBA data PNG'];}
function lmCompare(array $images,string $name):array {$canvas=new Imagick();$canvas->newImage(400*count($images),320,'white','png');$rects=[];foreach($images as $n=>$image){$canvas->compositeImage($image,Imagick::COMPOSITE_COPY,400*$n,0);$rects[]=['x'=>400*$n,'y'=>0,'width'=>400,'height'=>320];}lmSave($canvas,$name);return $rects;}
function lmTry(callable $fn):array {try{$value=$fn();return ['type'=>get_debug_type($value),'returned'=>$value];}catch(Throwable $e){return ['error'=>['class'=>get_class($e),'message'=>$e->getMessage()]];}}
function lmDiagnostic(string $id,array $data):void {
    $doc=['schema'=>'aen-legacy-map-program-diagnostics-v1','example_id'=>$id,'runtime'=>['php'=>PHP_VERSION,'imagick'=>phpversion('imagick'),'imagemagick'=>Imagick::getVersion()],'legacy_available'=>method_exists(Imagick::class,'mapImage'),'legacy_executed'=>false,'current_available'=>method_exists(Imagick::class,'remapImage'),'executed_api'=>'remapImage','dither_constants'=>['NO'=>Imagick::DITHERMETHOD_NO,'FLOYDSTEINBERG'=>Imagick::DITHERMETHOD_FLOYDSTEINBERG]]+$data;
    if(file_put_contents(__DIR__.'/metrics.json',json_encode($doc,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR)."\n")===false)throw new RuntimeException('Cannot save metrics');
}

lmRestore восстанавливает входы, lmSave записывает клон и действительно читает файл, lmAudit сравнивает его набор RGB с разрешённым. lmState раздельно измеряет RGB и alpha, поэтому включённый канал не подменяет проверку значений. lmView создаёт отдельный просмотр на клетках; lmPaletteView увеличивает четыре цветных пикселя для объяснения. Это сопровождение примеров, а минимальный полезный вызов расположен в начале главы.

Реальные вопросы, которые помогают выбрать сценарий

Обсуждения используются как источники практических проблем. Решения и код этой главы оригинальные; технический контракт и наблюдения опираются на PHP.net, закреплённые исходники и собственные выполненные примеры.

ImageMagick color mapping by different metric?: Выбор библиотекой ближайшего цвета не совпадает с ожидаемым художественным результатом. Не обещаем универсальную метрику качества; сравниваем реальный результат с выбранной палитрой, отличаем dithering от критерия близости.

Creating palette image on the fly for remap behind : palette is not reset each time: Создание палитры во временных файлах приводит к ошибке отсутствующего пути. Архив восстанавливает проверенные входы рядом с example.php, затем функция принимает уже открытый объект Imagick.

How do I ignore transparency when quantizing in ImageMagick like image editors do?: При ограничении цветов меняется прозрачность PNG, хотя требуется сохранить её. Раздельно проверяем RGB и alpha, возвращаем исходную alpha явным COPYALPHA и читаем сохранённый PNG снова.

Remap / posterize image colors using list of colors in imagemagick: Нужно заменить цвета по заданному списку, без сложной подготовки образца. Показываем простой образец4×1 и его четыре реальные краски; это данные для операции, а не пространственная карта постера.

create GIF image with fixed colormap using Image Magick: Старое устройство требует фиксированного порядка цветов в GIF, а обычное сопоставление меняет таблицу. Отделяем принадлежность RGB набору от требования к индексам; наш PNG не объявляется решением задачи фиксированной GIF-таблицы.

Convert jpg to png with predefined color codes?: После JPEG в карте появились близкие, но неправильные оттенки вместо утверждённых цветов. Проверяем RGB готового PNG против заданного набора; число цветов само по себе не задаёт нужные значения.

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

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

Imagick::mapImageЗаменить исторический mapImage и проверить PNG в заданной палитре

PECL imagick 2, PECL imagick 3 · imagick

Imagick::mapImage(Imagick $map, bool $dither): bool

Imagick::mapImage: исторический контракт и современный переход

Imagick::mapImage(Imagick $map, bool $dither): bool

mapImage заменяет цвета текущего изображения ближайшими цветами из другого изображения-образца. Метод устарел с Imagick 3.4.4; официальный мануал не рекомендует полагаться на него. Официальная доступность: PECL imagick 2, PECL imagick 3. В нашей сборке Imagick 3.8.1 с ImageMagick 7.1.1-46 method_exists вернул false: этот метод отсутствует. В исходниках 3.8.1 историческая ветка включается только при ImageMagick ниже 7 и если устаревший API не исключён при сборке. Это не означает, что mapImage есть в любой сборке Imagick 3.

АргументИсторическая записьСмысл
mapОбязательный ImagickИзображение-образец с допустимыми цветами. Это объект, а не массив строк, имя файла или карта координат.
ditherОбязательный boolfalse отключает dithering, true разрешает его в историческом API. Узор соседних пикселей передаёт промежуточный вид ограниченным набором цветов; флаг не задаёт современное имя алгоритма.

Официальные текстовые описания обоих параметров на странице mapImage пусты; приведённые пояснения опираются также на закреплённые исходники. Результат bool равен true при успехе. Изображение изменяется в объекте, на котором вызван метод: true не является новым Imagick и не доказывает, что файл уже записан. Официальный раздел ошибок указывает ImagickException. Исторические ветки исходников проверяют наличие изображения и переводят отказ библиотеки в исключение; их выполнение в ImageMagick 6 здесь не заявляется.

// Исторический вызов: не работает в проверенной сборке IM7.
$image->mapImage($palette, false);

// Современный вызов, действительно выполненный в примерах.
$image->remapImage($palette, Imagick::DITHERMETHOD_NO);

Современный remapImage получает Imagick replacement и int из DITHERMETHOD_*, возвращает true при успехе и меняет текущий растр. Официальный контракт этой альтернативы требует ImageMagick 6.4.5 или новее. Перенос не сводится к замене имени: старый bool нельзя оставлять вместо явного режима. В наших программах с declare(strict_types=1) передача true прямо в remapImage дала TypeError. В коде с иными правилами приведения ошибка может проявиться иначе; именованная константа убирает двусмысленность.

Для текущего remapImage реально проверены ImagickException при пустом исходнике и пустом образце, TypeError при массиве вместо Imagick и при bool вместо режима в строгом вызове. Это факты о текущем API; они не выдаются за выполненные тесты старого mapImage. Обработку ошибок следует строить вокруг реальных входов, поддерживаемой сборки и выбранной операции.

Границы: ближайший цвет библиотеки не обязан совпадать с любым художественным или перцептивным критерием; образец должен соответствовать задаче и цветовому пространству. Сопоставление не обещает заданный порядок индексов PNG/GIF, использование каждого цвета или одинаковую частоту цветов. Непрозрачный образец не является обещанием сохранить исходную alpha. Эти условия нужно проверять на настоящем выходе.

Связанные темы: полная карточка remapImage и отличие от quantizeImage/clutImage, палитровые индексы и последовательности, канал alpha и фон, цветовое пространство.

Заменить старый вызов и получить PNG в четырёх заданных цветах

Собственный постер сохранён как poster-limited.png размером 400×320. После повторного чтения все RGB принадлежат четырём цветам palette.png. Исходный постер и палитра не изменились. Выполнен remapImage; отсутствующий в этой сборке mapImage не вызывался.

Заменить старый вызов и получить PNG в четырёх заданных цветахПример для указанного окружения
<?php
declare(strict_types=1);
require __DIR__ . '/legacy-map-fixture.php';
lmRestore();
$source = new Imagick(__DIR__ . '/poster.png');
$palette = new Imagick(__DIR__ . '/palette.png');
$sticker = new Imagick(__DIR__ . '/sticker.png');
$sourceBefore = lmState($source);
$paletteBefore = lmState($palette);
$stickerBefore = lmState($sticker);
$sourceHash = hash_file('sha256', __DIR__ . '/poster.png');
$image = clone $source;
$ok = $image->remapImage($palette, Imagick::DITHERMETHOD_NO);
$saved = lmSave($image, 'poster-limited.png');
$read = new Imagick(__DIR__ . '/poster-limited.png');
$audit = lmAudit($read, $palette);
$views = [lmView($source),lmPaletteView($palette),lmView($read)];
$images = ['source'=>lmImage($views[0],'first-source8.png'),'palette'=>lmImage($views[1],'first-palette8.png'),'result'=>lmImage($views[2],'first-result8.png')];
$rects = lmCompare($views,'legacy-map-first.png');
lmDiagnostic('legacy-map-first',[
 'set_return'=>$ok,'mapped_state'=>lmState($image),'saved'=>$saved,'readback'=>lmState($read),'audit'=>$audit,'palette_view'=>['input_geometry'=>[4,1],'display_geometry'=>[400,320],'stripe_width'=>100,'order'=>['#182e43','#208494','#e2a040','#f6ecd6']],
 'images'=>$images,'panel_keys'=>['source','palette','result'],'panel_geometry'=>$rects,'source_before'=>$sourceBefore,'source_after'=>lmState($source),'source_sha256'=>$sourceHash,'source_unchanged'=>lmState($source)===$sourceBefore&&hash_file('sha256',__DIR__.'/poster.png')===$sourceHash,'palette_before'=>$paletteBefore,'palette_after'=>lmState($palette),
 'checks'=>['legacy_unavailable'=>!method_exists(Imagick::class,'mapImage'),'current_available'=>method_exists(Imagick::class,'remapImage'),'return_true'=>$ok===true,'only_reference_rgb'=>$audit['all_rgb_in_palette'],'four_colors'=>$audit['rgb_color_count']===4,'palette_unchanged'=>lmState($palette)===$paletteBefore,'geometry_preserved'=>$read->getImageGeometry()===['width'=>400,'height'=>320],'saved_pixels_equal'=>lmHash($image)===lmHash($read)]
]);
echo "legacy-map-first.png\nfirst-source8.png\nfirst-palette8.png\nfirst-result8.png\nposter-limited.png\nmetrics.json\nlegacy=unavailable current=remapImage return=true colors=4 palette-membership=passed\n";

Собственный постер сохранён как poster-limited.png размером 400×320. После повторного чтения все RGB принадлежат четырём цветам palette.png. Исходный постер и палитра не изменились. Выполнен remapImage; отсутствующий в этой сборке mapImage не вызывался. В архиве example.php, три файла .png.b64 и legacy-map-fixture.php. Распакуйте всё рядом и выполните php example.php: обычные poster.png, palette.png и sticker.png восстановятся автоматически. Полный helper приведён в конце главы. Проверено в PHP 8.5.11 / Imagick 3.8.1 / ImageMagick 7.1.1-46 Q16, Windows. Реально выполнен современный remapImage; исторический mapImage здесь недоступен и не запускался.

Ожидаемый вывод после настоящего запуска
legacy-map-first.png
first-source8.png
first-palette8.png
first-result8.png
poster-limited.png
metrics.json
legacy=unavailable current=remapImage return=true colors=4 palette-membership=passed

Экспортировать постер и прозрачную наклейку с проверкой готовых файлов

Готовы poster-web.png, прозрачный sticker-web.png и palette-export.json. Функция прочитала оба записанных файла, проверила принадлежность RGB палитре и сохранность alpha наклейки. JSON связывает палитру и выходные файлы по SHA256; он не является подписью автора.

Экспортировать постер и прозрачную наклейку с проверкой готовых файловПример для указанного окружения
<?php
declare(strict_types=1);
require __DIR__ . '/legacy-map-fixture.php';
lmRestore();
$source = new Imagick(__DIR__ . '/poster.png');
$palette = new Imagick(__DIR__ . '/palette.png');
$sticker = new Imagick(__DIR__ . '/sticker.png');
$sourceBefore = lmState($source);
$paletteBefore = lmState($palette);
$stickerBefore = lmState($sticker);
$sourceHash = hash_file('sha256', __DIR__ . '/poster.png');
function exportPalettePNG(
    Imagick $source,
    Imagick $palette,
    int $ditherMethod,
    bool $preserveAlpha,
    string $filename
): array {
    $result = clone $source;
    $result->remapImage($palette, $ditherMethod);
    if ($preserveAlpha) {
        $result->setImageAlphaChannel(Imagick::ALPHACHANNEL_SET);
        $result->compositeImage($source, Imagick::COMPOSITE_COPYALPHA, 0, 0);
    }
    $saved = lmSave($result, $filename, $preserveAlpha ? 6 : 2);
    $read = new Imagick(__DIR__ . '/' . $filename);
    $audit = lmAudit($read, $palette);
    if (!$audit['all_rgb_in_palette']) {
        throw new RuntimeException('Saved RGB is outside the approved palette');
    }
    if ($preserveAlpha && lmHash($read, 'A') !== lmHash($source, 'A')) {
        throw new RuntimeException('Saved transparency differs from the source');
    }
    return ['saved'=>$saved,'audit'=>$audit,'preserve_alpha'=>$preserveAlpha,
        'alpha_equal'=>lmHash($read,'A')===lmHash($source,'A')];
}
$exports = [
    'poster'=>exportPalettePNG($source,$palette,Imagick::DITHERMETHOD_FLOYDSTEINBERG,false,'poster-web.png'),
    'sticker'=>exportPalettePNG($sticker,$palette,Imagick::DITHERMETHOD_NO,true,'sticker-web.png'),
];
$manifest = [
    'palette_file'=>'palette.png',
    'palette_sha256'=>hash_file('sha256',__DIR__.'/palette.png'),
    'approved_rgb'=>lmRGBSet($palette),
    'exports'=>$exports,
    'note'=>'RGB membership does not promise indexed palette order or only four displayed colors after alpha compositing',
];
file_put_contents(__DIR__ . '/palette-export.json', json_encode(
    $manifest, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR
) . "\n");
$readManifest = json_decode((string)file_get_contents(__DIR__.'/palette-export.json'),true,512,JSON_THROW_ON_ERROR);
$posterRead = new Imagick(__DIR__.'/poster-web.png');
$stickerRead = new Imagick(__DIR__.'/sticker-web.png');
$views = [lmView($posterRead),lmView($stickerRead)];
$images = ['poster'=>lmImage($views[0],'workflow-poster8.png'),'sticker'=>lmImage($views[1],'workflow-sticker8.png')];
$rects = lmCompare($views,'legacy-map-workflow.png');
lmDiagnostic('legacy-map-workflow',[
 'exports'=>$exports,'manifest'=>$readManifest,'manifest_sha256'=>hash_file('sha256',__DIR__.'/palette-export.json'),'sticker_before'=>$stickerBefore,'sticker_after'=>lmState($sticker),'readback'=>['poster'=>lmState($posterRead),'sticker'=>lmState($stickerRead)],
 'images'=>$images,'panel_keys'=>['poster','sticker'],'panel_geometry'=>$rects,'source_before'=>$sourceBefore,'source_after'=>lmState($source),'source_sha256'=>$sourceHash,'source_unchanged'=>lmState($source)===$sourceBefore&&hash_file('sha256',__DIR__.'/poster.png')===$sourceHash,'palette_before'=>$paletteBefore,'palette_after'=>lmState($palette),
 'checks'=>['poster_reference_rgb'=>$exports['poster']['audit']['all_rgb_in_palette'],'sticker_reference_rgb'=>$exports['sticker']['audit']['all_rgb_in_palette'],'sticker_alpha_preserved'=>$exports['sticker']['alpha_equal'],'manifest_bound'=>$readManifest['palette_sha256']===hash_file('sha256',__DIR__.'/palette.png'),'manifest_file_hashes'=>$readManifest['exports']['poster']['saved']['sha256']===hash_file('sha256',__DIR__.'/poster-web.png')&&$readManifest['exports']['sticker']['saved']['sha256']===hash_file('sha256',__DIR__.'/sticker-web.png'),'sticker_unchanged'=>lmState($sticker)===$stickerBefore,'palette_unchanged'=>lmState($palette)===$paletteBefore]
]);
echo "legacy-map-workflow.png\nworkflow-poster8.png\nworkflow-sticker8.png\nposter-web.png\nsticker-web.png\npalette-export.json\nmetrics.json\nexports=2 approved-RGB=yes sticker-alpha=preserved manifest=read-back\n";

Готовы poster-web.png, прозрачный sticker-web.png и palette-export.json. Функция прочитала оба записанных файла, проверила принадлежность RGB палитре и сохранность alpha наклейки. JSON связывает палитру и выходные файлы по SHA256; он не является подписью автора. В архиве example.php, три файла .png.b64 и legacy-map-fixture.php. Распакуйте всё рядом и выполните php example.php: обычные poster.png, palette.png и sticker.png восстановятся автоматически. Полный helper приведён в конце главы. Проверено в PHP 8.5.11 / Imagick 3.8.1 / ImageMagick 7.1.1-46 Q16, Windows. Реально выполнен современный remapImage; исторический mapImage здесь недоступен и не запускался.

Ожидаемый вывод после настоящего запуска
legacy-map-workflow.png
workflow-poster8.png
workflow-sticker8.png
poster-web.png
sticker-web.png
palette-export.json
metrics.json
exports=2 approved-RGB=yes sticker-alpha=preserved manifest=read-back
Источники и обсуждения

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

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

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

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