Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Imagick v6 to v7 upgrade #676

Open
rusinowiczjakub opened this issue Jun 17, 2024 · 5 comments
Open

Imagick v6 to v7 upgrade #676

rusinowiczjakub opened this issue Jun 17, 2024 · 5 comments

Comments

@rusinowiczjakub
Copy link

rusinowiczjakub commented Jun 17, 2024

I have a class like this which i used to fill specific mask with image:

<?php

declare(strict_types=1);

namespace ImageProcessing\Infrastructure\Service\Instruction\Imagick\Operation;

use Imagick;
use ImagickException;

class FillMaskOperation
{
    /** @throws ImagickException */
    public static function fill(Imagick $fill, Imagick $mask, int $x = 0, int $y = 0): Imagick
    {
  /*
   * Both the mask and fill can have different shapes, for example, an image after FIT scaling may not fully cover the mask.
   * We need to adjust the original mask so that it does not contain areas that are not available in the fill,
   * - meaning we perform the intersection of these two images.
   * To do this, we need to convert the fill into a mask, and then extract the common part (intersection) between
   * the original mask and the fill mask. Only the resulting mask will be used to generate the result.
   *
   * Steps:
   * - Clone the mask object that we will modify, to avoid modifying the original.
   * - Disable the ALPHA channel in the cloned mask, which will turn transparent pixels into black
   *   (masks are grayscale, and black represents transparent pixels).
   * - Clone the fill to avoid modifying the original image.
   * - Convert the fill to a mask (similar to what we do when creating a stroke).
   * - Using `COMPOSITE_DARKEN`, extract the intersection between the mask and the fill mask.
   * - Use the original fill and the new intersection mask to generate the final result.
   */
        $result = clone $fill;
        $fillMask = clone $fill;
        $clonedMask = clone $mask;

         //Making transparent pixels black
        $clonedMask->setImageAlphaChannel(Imagick::ALPHACHANNEL_DEACTIVATE);

        $fillMask->separateImageChannel(Imagick::CHANNEL_ALPHA);

        $fillMask->negateImage(true);

        $clonedMask->compositeImage($fillMask, Imagick::COMPOSITE_DARKEN, -$x, -$y);

        $result->compositeImage($clonedMask, Imagick::COMPOSITE_COPYOPACITY, $x, $y);

        return $result;
    }
}

Result of this code was that i had a mask, which was in general image where white part of image was object to fill, and black part of image was part to replace it with opacity. For when we wanted to fill image like this for example with gradient:

tux

it was creating mask from image:
mask
e:

and was filling it with fill like this:

fill

and result was like this:

result

Now code is not working and for example for image which is created from parts and in imagick v6 was looking like this:

Flow-Preview-1

Final result looks like this:
Flow-Preview-1

what should i do to achieve the previous effect?

@Danack
Copy link
Collaborator

Danack commented Jun 17, 2024

Please can you provide me with a Short, Self Contained, Correct (Compilable), Example so that I can run it and experiment with the code?

tbh, I still don't fully understand all of the changes that were made between ImageMagick 6 and 7, particularly around the change from an 'opacity' model to a 'transparency' model, so I won't be able to just look at what you're doing and say the fix.

@rusinowiczjakub
Copy link
Author

<?php

declare(strict_types=1);

$width = 200;
$height = 200;

$fillImagePath = __DIR__ . '/Asset/Image/iphone.webp';
$figureImagePath = __DIR__ . '/tux.png';

$figureImage = new Imagick($figureImagePath);

$textImage = createText('Sample');

$examples = [
    'circle_gradient' => [
        'mask' => createCircle($width, $height),
        'fill' => createGradientFill($width, $height, 'red-blue'),
    ],
    'line_gradient' => [
        'mask' => createLine($width, 10, 45),
        'fill' => createGradientFill($width, $height, 'red-blue'),
    ],
    'triangle_gradient' => [
        'mask' => createTriangle($width, $height),
        'fill' => createGradientFill($width, $height, 'red-blue'),
    ],
    'text_gradient' => [
        'mask' => clone $textImage,
        'fill' => createGradientFill($textImage->getImageWidth(), $textImage->getImageHeight(), 'red-blue'),
    ],
    'circle_image' => [
        'mask' => createCircle($width, $height),
        'fill' => createImageFill($width, $height, $fillImagePath),
    ],
    'figure_gradient' => [
        'mask' => createMaskFrom($figureImage),
        'fill' => createGradientFill($figureImage->getImageWidth(), $figureImage->getImageHeight(), 'red-blue'),
    ],
];

foreach ($examples as $name => $example) {
    echo sprintf("Creating %s\n", $name);

    writeImage($example['mask'], createDestinationPath($name . '/mask'));
    writeImage($example['fill'], createDestinationPath($name . '/fill'));

    $result = applyMaskToFill($example['fill'], $example['mask']);

    writeImage($result, createDestinationPath($name . '/result'));
}

function createCircle($width, $height): Imagick
{
    $figure = new Imagick();
    $figure->newPseudoImage($width, $height, 'canvas:none');

    $draw = new ImagickDraw();
    $draw->setFillColor(new ImagickPixel('white'));
    $draw->circle($width / 2, $height / 2, min($width / 2, $width / 2), 1);

    $figure->drawImage($draw);

    return $figure;
}

function createLine($width, $height, $rotate = 0): Imagick
{
    $figure = new Imagick();
    $figure->newPseudoImage($width, $height, 'canvas:none');

    $draw = new ImagickDraw();

    $draw->setFillColor(new ImagickPixel('white'));
    $draw->line(0, 0, $width, $height);
    $draw->rotate($rotate);

    $figure->drawImage($draw);

    return $figure;
}

function createTriangle($width, $height): Imagick
{
    $figure = new Imagick();
    $figure->newPseudoImage($width, $height, 'canvas:none');

    $draw = new ImagickDraw();

    $draw->setFillColor(new ImagickPixel('white'));
    $draw->polygon(
        [
            ['x' => 0, 'y' => $height],
            ['x' => $width / 2, 'y' => 0],
            ['x' => $width, 'y' => $height],
        ]
    );

    $figure->drawImage($draw);

    return $figure;
}

function createText(string $text): Imagick
{
    $figure = new Imagick();

    $draw = new ImagickDraw();

    $draw->setFillColor(new ImagickPixel('white'));
    $draw->setFont(__DIR__ . '/DejaVuSans.ttf');
    $draw->setFontSize(70);

    $metrics = $figure->queryFontMetrics($draw, $text);
    $textWidth = intval(ceil($metrics['textWidth']));
    $textHeight = intval(ceil($metrics['textHeight']));

    $figure->newPseudoImage($textWidth, $textHeight, 'canvas:none');

    $figure->annotateImage($draw, 0, $metrics['ascender'], 0, $text);

    $figure->drawImage($draw);

    return $figure;
}

function createGradientFill($width, $height, $pseudoColor): Imagick
{
    $fill = new Imagick();
    $fill->newPseudoImage($width, $height, 'gradient:' . $pseudoColor);

    return $fill;
}

function createImageFill($width, $height, $imagePath): Imagick
{
    $fill = new Imagick($imagePath);
    $fill->resizeImage($width, $height, Imagick::FILTER_GAUSSIAN, 1);

    return $fill;
}

function createMaskFrom($imagick): Imagick
{
    $mask = clone $imagick;

    $mask->separateImageChannel(Imagick::CHANNEL_ALPHA);
    $mask->negateImage(true); // potrzebne, bo poprzednia funkcja zwraca, że biały to tło, a czarny to figura

    return $mask;
}

function applyMaskToFill(Imagick $fill, Imagick $mask): Imagick
{
    $result = clone $fill;

    // Klonujemy, bo zmieniamy alpha channel, a nie chcemy modyfikować źródła:
    $clonedMask = clone $mask;
    $clonedMask->setImageAlphaChannel(Imagick::ALPHACHANNEL_DEACTIVATE);

    $result->compositeImage($clonedMask, Imagick::COMPOSITE_COPYOPACITY, 0, 0);

    return $result;
}

function writeImage(Imagick $image, string $path): void
{
    $image->setImageFormat('png');
    $image->writeImage($path);
}

function createDestinationPath(string $suffix = ''): string
{
    $baseDir = __DIR__ . '/output/fill_figure';

    $path = sprintf('%s/%s_copy.png', $baseDir, $suffix);

    $dir = dirname($path);

    if (!is_dir($dir)) {
        mkdir($dir, 0755, true);
    }

    return $path;
}

That's small working example. You can use any images as fillImagePath and figureImagePath

@Danack
Copy link
Collaborator

Danack commented Jun 17, 2024

Thanks. Using the image of tux as $figureImagePath and using the reb-blue gradient as $fillImagePath, please can you post the example output you want in ImageMagick 6, and tell me which of the output directories I should be looking at the compare the result?

@rusinowiczjakub
Copy link
Author

rusinowiczjakub commented Jun 18, 2024

Result of fill_figure > figure_gradient in imagick v6:
result

Same result in imagick v7:
result_copy

You can check out also fill_figure > line_gradient:

Imagick 6:
result

Imagick 7:
result_copy

I think it is problem with negating image, and how alpha channels work in imagick v7, but have no idea how to fix this. Was trying different options with passing specific channels to negateImage method

@Danack
Copy link
Collaborator

Danack commented Jun 18, 2024

Okay, so the code below works for the provided images.

However it is possible that you actually want it to work with more generic 'images with a plain background' rather than just 'images with already set alpha channels'. If that's the case, please can you provide an updated input image to use.

I think what is happening is that there are some subtle assumptions about what to do when you composite images that have different numbers of channels, and some of those assumptions are different in IM6 vs IM7. Which is a bit of a nightmare tbh.

<?php


declare(strict_types=1);

$width = 200;
$height = 200;


$figureImagePath = __DIR__ . '/tux.png';

$figureImage = new Imagick($figureImagePath);

$name = 'dja';

$example_mask = clone $figureImage;
$example_mask->separateImageChannel(Imagick::CHANNEL_ALPHA);

$example_fill = createGradientFill($figureImage->getImageWidth(), $figureImage->getImageHeight(), 'red-blue');

echo sprintf("Creating %s\n", $name);

writeImage($example_mask, createDestinationPath($name . '/mask', 0));
writeImage($example_fill, createDestinationPath($name . '/fill', 0));

$result = clone $example_fill;
$clonedMask = clone $example_mask;

$result->compositeImage($clonedMask, Imagick::COMPOSITE_COPYOPACITY, 0, 0);
//$result->compositeImage($clonedMask, Imagick::COMPOSITE_COPYALPHA, 0, 0);

writeImage($result, createDestinationPath($name . '/result'));

function createGradientFill($width, $height, $pseudoColor): Imagick
{
    $fill = new Imagick();
    $fill->newPseudoImage($width, $height, 'gradient:' . $pseudoColor);

    return $fill;
}

function writeImage(Imagick $image, string $path): void
{
    $image->setImageFormat('png');
    $image->writeImage($path);
}

function createDestinationPath(string $suffix): string
{
    $baseDir = __DIR__ . '/output/fill_figure';

    $path = sprintf('%s/%s_copy.png', $baseDir, $suffix);

    $dir = dirname($path);

    if (!is_dir($dir)) {
        mkdir($dir, 0755, true);
    }

    return $path;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants