From a42b7d85458a0d3db8beff7f512f18117fb183a7 Mon Sep 17 00:00:00 2001 From: Constantine Nathanson Date: Tue, 14 Nov 2023 18:56:33 +0200 Subject: [PATCH] Add support for AI generative effects --- src/Transformation/Effect/EffectAction.php | 2 +- .../Effect/Generative/DetectMultipleTrait.php | 30 ++++++ .../Effect/Generative/GenerativeEffect.php | 24 +++++ .../Generative/GenerativeEffectAction.php | 22 +++++ .../Generative/GenerativeEffectTrait.php | 77 +++++++++++++++ .../Effect/Generative/GenerativeRecolor.php | 59 +++++++++++ .../Effect/Generative/GenerativeRemove.php | 83 ++++++++++++++++ .../Effect/Generative/GenerativeReplace.php | 96 ++++++++++++++++++ .../Effect/Generative/PromptTrait.php | 31 ++++++ .../Effect/ImageEffectTrait.php | 1 + .../Effect/ListEffectQualifier.php | 7 +- .../Effect/Pixel/BackgroundRemoval.php | 4 +- .../Positioning/PixelEffectRegionTrait.php | 17 +++- .../QualifierValue/Canvas/RectangleRegion.php | 38 ++++++++ .../FullListExpressionQualifierMultiValue.php | 5 +- .../FullListQualifierMultiValue.php | 42 ++++++++ .../ListQualifierMultiValue.php | 25 +++++ .../QualifierValue/QualifierMultiValue.php | 2 +- src/Utils/TransformationUtils.php | 17 +++- .../Unit/Transformation/Common/EffectTest.php | 2 +- .../Image/GenerativeEffectTest.php | 97 +++++++++++++++++++ 21 files changed, 669 insertions(+), 12 deletions(-) create mode 100644 src/Transformation/Effect/Generative/DetectMultipleTrait.php create mode 100644 src/Transformation/Effect/Generative/GenerativeEffect.php create mode 100644 src/Transformation/Effect/Generative/GenerativeEffectAction.php create mode 100644 src/Transformation/Effect/Generative/GenerativeEffectTrait.php create mode 100644 src/Transformation/Effect/Generative/GenerativeRecolor.php create mode 100644 src/Transformation/Effect/Generative/GenerativeRemove.php create mode 100644 src/Transformation/Effect/Generative/GenerativeReplace.php create mode 100644 src/Transformation/Effect/Generative/PromptTrait.php create mode 100644 src/Transformation/Qualifier/QualifierValue/Canvas/RectangleRegion.php create mode 100644 src/Transformation/Qualifier/QualifierValue/FullListQualifierMultiValue.php create mode 100644 src/Transformation/Qualifier/QualifierValue/ListQualifierMultiValue.php create mode 100644 tests/Unit/Transformation/Image/GenerativeEffectTest.php diff --git a/src/Transformation/Effect/EffectAction.php b/src/Transformation/Effect/EffectAction.php index 9cfe2b8..bbcdcc0 100644 --- a/src/Transformation/Effect/EffectAction.php +++ b/src/Transformation/Effect/EffectAction.php @@ -29,6 +29,6 @@ class EffectAction extends Action */ public function __construct($effect, ...$args) { - parent::__construct(ClassUtils::verifyInstance($effect, EffectQualifier::class, null, ...$args)); + parent::__construct(ClassUtils::verifyInstance($effect, static::MAIN_QUALIFIER, null, ...$args)); } } diff --git a/src/Transformation/Effect/Generative/DetectMultipleTrait.php b/src/Transformation/Effect/Generative/DetectMultipleTrait.php new file mode 100644 index 0000000..4f77b92 --- /dev/null +++ b/src/Transformation/Effect/Generative/DetectMultipleTrait.php @@ -0,0 +1,30 @@ +getMainQualifier()->getPropertiesValue()->setSimpleNamedValue( + GenerativeEffectAction::MULTIPLE, + TransformationUtils::boolToString($detectMultiple) + ); + + return $this; + } +} diff --git a/src/Transformation/Effect/Generative/GenerativeEffect.php b/src/Transformation/Effect/Generative/GenerativeEffect.php new file mode 100644 index 0000000..4b9ae26 --- /dev/null +++ b/src/Transformation/Effect/Generative/GenerativeEffect.php @@ -0,0 +1,24 @@ +prompt($prompt); + $this->toColor($toColor); + $this->detectMultiple($detectMultiple); + } + + /** + * Sets the target color. + * + * @param string|ColorValue $toColor The HTML name or RGB/A hex code of the target color. + * + * @return $this + */ + public function toColor($toColor) + { + $this->getMainQualifier()->getPropertiesValue()->setSimpleNamedValue( + self::TO_COLOR, + StringUtils::truncatePrefix((string)$toColor, '#') + ); + + return $this; + } +} diff --git a/src/Transformation/Effect/Generative/GenerativeRemove.php b/src/Transformation/Effect/Generative/GenerativeRemove.php new file mode 100644 index 0000000..1386f2c --- /dev/null +++ b/src/Transformation/Effect/Generative/GenerativeRemove.php @@ -0,0 +1,83 @@ +prompt($prompt); + $this->region($region); + $this->detectMultiple($detectMultiple); + $this->removeShadow($removeShadow); + } + + /** + * Sets the target region. + * + * @param $region + * + * @return $this + */ + public function region(...$region) + { + $this->getMainQualifier()->getPropertiesValue()->setSimpleNamedValue( + self::REGION, + new FullListQualifierMultiValue( + ...ArrayUtils::build($region) + ) + ); + + return $this; + } + + /** + * Whether to remove the shadow in addition to the object(s). + * + * @param bool $removeShadow Whether to remove shadow. + * + * @return $this + */ + public function removeShadow($removeShadow = true) + { + $this->getMainQualifier()->getPropertiesValue()->setSimpleNamedValue( + self::REMOVE_SHADOW, + TransformationUtils::boolToString($removeShadow) + ); + + return $this; + } +} diff --git a/src/Transformation/Effect/Generative/GenerativeReplace.php b/src/Transformation/Effect/Generative/GenerativeReplace.php new file mode 100644 index 0000000..759ad69 --- /dev/null +++ b/src/Transformation/Effect/Generative/GenerativeReplace.php @@ -0,0 +1,96 @@ +fromPrompt($fromPrompt); + $this->toPrompt($toPrompt); + $this->preserveGeometry($preserveGeometry); + $this->detectMultiple($detectMultiple); + } + + /** + * Use natural language to describe what you want to affect in the image. + * + * @param string $fromPrompt A description of the object to replace. + * + * @return $this + */ + public function fromPrompt($fromPrompt) + { + $this->getMainQualifier()->getPropertiesValue()->setSimpleNamedValue( + self::FROM_PROMPT, + $fromPrompt + ); + + return $this; + } + + /** + * Use natural language to describe what you want to affect in the image. + * + * @param string $toPrompt A description of the replacement object. + * + * @return $this + */ + public function toPrompt($toPrompt) + { + $this->getMainQualifier()->getPropertiesValue()->setSimpleNamedValue( + self::TO_PROMPT, + $toPrompt + ); + + return $this; + } + + /** + * Preserve geometry. + * + * @param bool $preserveGeometry Whether to maintain the shape of the object you're replacing. + * + * @return $this + */ + public function preserveGeometry($preserveGeometry = true) + { + $this->getMainQualifier()->getPropertiesValue()->setSimpleNamedValue( + self::PRESERVE_GEOMETRY, + TransformationUtils::boolToString($preserveGeometry) + ); + + return $this; + } +} diff --git a/src/Transformation/Effect/Generative/PromptTrait.php b/src/Transformation/Effect/Generative/PromptTrait.php new file mode 100644 index 0000000..249c485 --- /dev/null +++ b/src/Transformation/Effect/Generative/PromptTrait.php @@ -0,0 +1,31 @@ +getMainQualifier()->getPropertiesValue()->setSimpleNamedValue( + GenerativeEffectAction::PROMPT, + new FullListQualifierMultiValue( + ...ArrayUtils::build($prompt) + ) + ); + + return $this; + } +} diff --git a/src/Transformation/Effect/ImageEffectTrait.php b/src/Transformation/Effect/ImageEffectTrait.php index a0e95ed..9f4ce63 100644 --- a/src/Transformation/Effect/ImageEffectTrait.php +++ b/src/Transformation/Effect/ImageEffectTrait.php @@ -22,4 +22,5 @@ trait ImageEffectTrait use MiscEffectTrait; use AddonEffectTrait; use ThemeEffectTrait; + use GenerativeEffectTrait; } diff --git a/src/Transformation/Effect/ListEffectQualifier.php b/src/Transformation/Effect/ListEffectQualifier.php index de50307..b23af1d 100644 --- a/src/Transformation/Effect/ListEffectQualifier.php +++ b/src/Transformation/Effect/ListEffectQualifier.php @@ -16,6 +16,11 @@ */ class ListEffectQualifier extends EffectQualifier { + /** + * @var string VALUE_CLASS The class of the qualifier value. Can be customized by derived classes. + */ + const VALUE_CLASS = QualifierMultiValue::class; + const PROPERTIES = 'properties'; /** @@ -28,7 +33,7 @@ public function __construct($effectName, ...$values) { parent::__construct($effectName); - $this->getValue()->setSimpleValue(self::PROPERTIES, new ListExpressionQualifierMultiValue(...$values)); + $this->getValue()->setSimpleValue(self::PROPERTIES, new ListQualifierMultiValue(...$values)); } /** diff --git a/src/Transformation/Effect/Pixel/BackgroundRemoval.php b/src/Transformation/Effect/Pixel/BackgroundRemoval.php index a42486c..aa86324 100644 --- a/src/Transformation/Effect/Pixel/BackgroundRemoval.php +++ b/src/Transformation/Effect/Pixel/BackgroundRemoval.php @@ -10,6 +10,8 @@ namespace Cloudinary\Transformation; +use Cloudinary\TransformationUtils; + /** * Class BackgroundRemoval * @@ -51,7 +53,7 @@ public function __construct($fineEdges = null, $hints = []) */ public function fineEdges($fineEdges = true) { - $value = $fineEdges === true ? 'y' : ($fineEdges === false ? 'n' : $fineEdges); + $value = TransformationUtils::boolToString($fineEdges, 'y', 'n'); $this->getMainQualifier()->getPropertiesValue()->setSimpleNamedValue(self::FINE_EDGES, $value); diff --git a/src/Transformation/Positioning/PixelEffectRegionTrait.php b/src/Transformation/Positioning/PixelEffectRegionTrait.php index dcfa83d..fff040a 100644 --- a/src/Transformation/Positioning/PixelEffectRegionTrait.php +++ b/src/Transformation/Positioning/PixelEffectRegionTrait.php @@ -11,7 +11,7 @@ namespace Cloudinary\Transformation; /** - * Trait RegionTrait + * Trait PixelEffectRegionTrait */ trait PixelEffectRegionTrait { @@ -49,4 +49,19 @@ public static function custom($x = null, $y = null, $width = null, $height = nul { return new Region($x, $y, $width, $height); } + + /** + * Returns the rectangle region. + * + * @param int $x X. + * @param int $y Y. + * @param int $width Width. + * @param int $height Height. + * + * @return RectangleRegion + */ + public static function rectangle($x = null, $y = null, $width = null, $height = null) + { + return new RectangleRegion($x, $y, $width, $height); + } } diff --git a/src/Transformation/Qualifier/QualifierValue/Canvas/RectangleRegion.php b/src/Transformation/Qualifier/QualifierValue/Canvas/RectangleRegion.php new file mode 100644 index 0000000..969d570 --- /dev/null +++ b/src/Transformation/Qualifier/QualifierValue/Canvas/RectangleRegion.php @@ -0,0 +1,38 @@ +namedArguments) + count($this->arguments) > 1 + && StringUtils::contains($string, self::VALUE_DELIMITER) + ) { return '(' . $string . ')'; } diff --git a/src/Transformation/Qualifier/QualifierValue/FullListQualifierMultiValue.php b/src/Transformation/Qualifier/QualifierValue/FullListQualifierMultiValue.php new file mode 100644 index 0000000..f1ff033 --- /dev/null +++ b/src/Transformation/Qualifier/QualifierValue/FullListQualifierMultiValue.php @@ -0,0 +1,42 @@ +namedArguments) + count($this->arguments) > 1 + && StringUtils::contains($string, self::VALUE_DELIMITER) + ) { + return '(' . $string . ')'; + } + + return $string; + } +} diff --git a/src/Transformation/Qualifier/QualifierValue/ListQualifierMultiValue.php b/src/Transformation/Qualifier/QualifierValue/ListQualifierMultiValue.php new file mode 100644 index 0000000..dfc50ae --- /dev/null +++ b/src/Transformation/Qualifier/QualifierValue/ListQualifierMultiValue.php @@ -0,0 +1,25 @@ +arguments, $this->argumentOrder) ); $namedValues = ArrayUtils::implodeAssoc( - ArrayUtils::sortByArray($this->namedArguments), + ArrayUtils::sortByArray($this->namedArguments, $this->argumentOrder), static::VALUE_DELIMITER, static::KEY_VALUE_DELIMITER ); diff --git a/src/Utils/TransformationUtils.php b/src/Utils/TransformationUtils.php index 450a3fa..c818fda 100644 --- a/src/Utils/TransformationUtils.php +++ b/src/Utils/TransformationUtils.php @@ -53,11 +53,18 @@ public static function floatToString($value) */ public static function boolToIntString($value) { - if (! is_bool($value)) { - return $value; - } - - return $value ? '1' : '0'; + return self::boolToString($value, '1', '0'); + } + /** + * Helper method for converting boolean to any representation as string. + * + * @param mixed $value Candidate to convert. If not boolean, returned as is + * + * @return string + */ + public static function boolToString($value, $trueString = 'true', $falseString = 'false') + { + return is_bool($value) ? ($value ? $trueString : $falseString) : $value; } diff --git a/tests/Unit/Transformation/Common/EffectTest.php b/tests/Unit/Transformation/Common/EffectTest.php index 22b638b..0b6653b 100644 --- a/tests/Unit/Transformation/Common/EffectTest.php +++ b/tests/Unit/Transformation/Common/EffectTest.php @@ -302,7 +302,7 @@ public function testBackgroundRemoval() ); self::assertEquals( - 'e_background_removal:fineedges_y;hints_(cat)', + 'e_background_removal:fineedges_y;hints_cat', (string)Effect::backgroundRemoval()->fineEdges()->hints(ForegroundObject::cat()) ); diff --git a/tests/Unit/Transformation/Image/GenerativeEffectTest.php b/tests/Unit/Transformation/Image/GenerativeEffectTest.php new file mode 100644 index 0000000..e2e691f --- /dev/null +++ b/tests/Unit/Transformation/Image/GenerativeEffectTest.php @@ -0,0 +1,97 @@ +detectMultiple() + ); + + self::assertEquals( + 'e_gen_recolor:multiple_false;prompt_green tree;to-color_f0c', + (string)Effect::generativeRecolor("green tree", "#f0c")->detectMultiple(false) + ); + } + + public function testGenerativeRemove() + { + self::assertEquals( + 'e_gen_remove:prompt_(sweater;dog;earring)', + (string)Effect::generativeRemove(["sweater", "dog", "earring"]) + ); + + self::assertEquals( + 'e_gen_remove:region_(h_3500;w_1900;x_300;y_200)', + (string)Effect::generativeRemove()->region(Region::rectangle(300, 200, 1900, 3500)) + ); + + self::assertEquals( + 'e_gen_remove:region_((h_200;w_200;x_100;y_500);(h_50;w_50;x_1200;y_1500))', + (string)Effect::generativeRemove()->region( + Region::rectangle(100, 500, 200, 200), + Region::rectangle(1200, 1500, 50, 50) + ) + ); + + self::assertEquals( + 'e_gen_remove:multiple_true;prompt_tree;remove-shadow_false', + (string)Effect::generativeRemove(["tree"])->detectMultiple()->removeShadow(false) + ); + + self::assertEquals( + 'e_gen_remove:multiple_false;prompt_green tree;remove-shadow_true', + (string)Effect::generativeRemove("green tree")->detectMultiple(false)->removeShadow() + ); + } + + public function testGenerativeReplace() + { + self::assertEquals( + 'e_gen_replace:from_sunny sky;to_dark sky', + (string)Effect::generativeReplace('sunny sky', 'dark sky') + ); + + self::assertEquals( + 'e_gen_replace:from_balloon;multiple_false;preserve-geometry_true;to_airplane', + (string)Effect::generativeReplace('balloon', 'airplane')->preserveGeometry()->detectMultiple(false) + ); + + self::assertEquals( + 'e_gen_replace:from_balloon;multiple_true;preserve-geometry_false;to_airplane', + (string)Effect::generativeReplace('balloon', 'airplane')->preserveGeometry(false)->detectMultiple() + ); + } +}