├── assets ├── heart.png └── fork-logo.png ├── src ├── exception │ ├── NotAnImageException.php │ ├── ImageNotSavedException.php │ └── NotALocalFileException.php ├── connectors │ ├── ConnectorInterface.php │ ├── translation │ │ ├── TranslatorInterface.php │ │ ├── HuggingFaceT5SmallTranslator.php │ │ ├── HuggingFaceOpusMtEnDeTranslator.php │ │ ├── AbstractHuggingFaceTranslator.php │ │ └── DeeplTranslator.php │ ├── alttextgeneration │ │ ├── AltTextGeneratorInterface.php │ │ ├── HuggingFaceBlipBaseAltTextGenerator.php │ │ ├── HuggingFaceBlipLargeAltTextGenerator.php │ │ └── AbstractHuggingFaceAltTextGenerator.php │ └── AbstractHuggingFaceConnector.php ├── events │ ├── RegisterGeneratorsEvent.php │ └── RegisterTranslatorsEvent.php ├── translations │ └── de │ │ └── altify.php ├── services │ ├── AbstractConnectorService.php │ ├── Generator.php │ └── Translator.php ├── helpers │ └── AssetHelper.php ├── jobs │ └── GenerateAltText.php ├── controllers │ ├── GenerateAltTextController.php │ └── TranslateAltTextController.php ├── templates │ └── _settings.twig ├── icon-mask.svg ├── elements │ └── actions │ │ ├── GenerateAltText.php │ │ └── TranslateAltText.php ├── models │ └── Settings.php ├── Plugin.php └── icon.svg ├── LICENSE.md └── composer.json /assets/heart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fork/craft-altify/main/assets/heart.png -------------------------------------------------------------------------------- /assets/fork-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fork/craft-altify/main/assets/fork-logo.png -------------------------------------------------------------------------------- /src/exception/NotAnImageException.php: -------------------------------------------------------------------------------- 1 | 'Alt text generieren', 5 | 'Generators' => 'Generatoren', 6 | 'Translate alt text' => 'Alt text übersetzen', 7 | 'Translators' => 'Übersetzer', 8 | '{class} must implement {interface}' => '{class} muss {interface} implementieren', 9 | ]; 10 | -------------------------------------------------------------------------------- /src/connectors/translation/TranslatorInterface.php: -------------------------------------------------------------------------------- 1 | De'; 8 | protected string $handle = 't5SmallEnDe'; 9 | protected string $modelPath = '/models/google-t5/t5-small'; 10 | } 11 | -------------------------------------------------------------------------------- /src/connectors/translation/HuggingFaceOpusMtEnDeTranslator.php: -------------------------------------------------------------------------------- 1 | De'; 8 | protected string $handle = 'opusMtEnDe'; 9 | protected string $modelPath = '/models/Helsinki-NLP/opus-mt-en-de'; 10 | } 11 | -------------------------------------------------------------------------------- /src/connectors/alttextgeneration/HuggingFaceBlipBaseAltTextGenerator.php: -------------------------------------------------------------------------------- 1 | getHandle()] = $classname; 19 | } 20 | 21 | return $data; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/connectors/AbstractHuggingFaceConnector.php: -------------------------------------------------------------------------------- 1 | name; 16 | } 17 | 18 | public function getHandle(): string 19 | { 20 | return $this->handle; 21 | } 22 | 23 | protected function getClient(): Client 24 | { 25 | return new Client([ 26 | 'base_uri' => "https://api-inference.huggingface.co", 27 | 'headers' => [ 28 | 'Authorization' => 'Bearer ' . Plugin::getInstance()->getSettings()->getHuggingFaceApiToken(), 29 | ], 30 | ]); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/helpers/AssetHelper.php: -------------------------------------------------------------------------------- 1 | kind !== Asset::KIND_IMAGE) { 27 | throw new NotAnImageException("Asset is not an image"); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/jobs/GenerateAltText.php: -------------------------------------------------------------------------------- 1 | generator->generateAltTextForImage($this->assetId); 32 | } 33 | 34 | protected function defaultDescription(): ?string 35 | { 36 | return "Generate alt text"; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/connectors/translation/AbstractHuggingFaceTranslator.php: -------------------------------------------------------------------------------- 1 | alt; 19 | 20 | if (!empty($altText)) { 21 | $client = $this->getClient(); 22 | 23 | $response = $client->post( 24 | $this->modelPath, 25 | ['json' => ['inputs' => $image->alt]] 26 | ); 27 | 28 | $body = $response->getBody(); 29 | $decoded = json_decode($body, true); 30 | $altText = !empty($decoded) ? $decoded[0]['translation_text'] : null; 31 | } 32 | 33 | return $altText; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Fork Unstable Media GmbH. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/connectors/translation/DeeplTranslator.php: -------------------------------------------------------------------------------- 1 | name; 18 | } 19 | 20 | public function getHandle(): string 21 | { 22 | return $this->handle; 23 | } 24 | 25 | /** 26 | * @param Asset $image 27 | * @return string|null 28 | * @throws DeepLException 29 | */ 30 | public function translateAltTextForImage(Asset $image): ?string 31 | { 32 | $altText = $image->alt; 33 | 34 | if (!empty($altText)) { 35 | $translator = new Translator(Plugin::getInstance()->getSettings()->getDeeplApiKey()); 36 | $result = $translator->translateText($altText, null, $image->site->language); 37 | $altText = $result->text; 38 | } 39 | 40 | return $altText; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/controllers/GenerateAltTextController.php: -------------------------------------------------------------------------------- 1 | requireLogin(); 31 | $this->requireCpRequest(); 32 | $assetId = Craft::$app->request->getBodyParam('assetId'); 33 | 34 | try { 35 | Plugin::getInstance()->generator->generateAltTextForImage($assetId); 36 | } catch (Exception|Throwable $e) { 37 | Craft::$app->getSession()->setError($e->getMessage()); 38 | } 39 | 40 | return $this->asSuccess(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/controllers/TranslateAltTextController.php: -------------------------------------------------------------------------------- 1 | requireLogin(); 31 | $this->requireCpRequest(); 32 | $assetId = Craft::$app->request->getBodyParam('assetId'); 33 | 34 | try { 35 | Plugin::getInstance()->translator->translateAltTextForImage($assetId); 36 | } catch (Exception|Throwable $e) { 37 | Craft::$app->getSession()->setError($e->getMessage()); 38 | } 39 | 40 | return $this->asSuccess(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/templates/_settings.twig: -------------------------------------------------------------------------------- 1 | {# @var plugin \fork\altify\Plugin #} 2 | {# @var settings \fork\altify\models\Settings #} 3 | 4 | {% import '_includes/forms.twig' as forms %} 5 | 6 | {{ forms.autosuggestField({ 7 | label: "Alt text generator service"|t('altify'), 8 | id: 'altTextGenerator', 9 | name: 'altTextGenerator', 10 | suggestEnvVars: true, 11 | suggestions: settings.generatorSuggestions, 12 | value: settings.altTextGenerator, 13 | size: 60, 14 | errors: settings.getErrors('altTextGenerator'), 15 | }) }} 16 | 17 | {{ forms.autosuggestField({ 18 | label: "Alt text translator service"|t('altify'), 19 | id: 'altTextTranslator', 20 | name: 'altTextTranslator', 21 | suggestEnvVars: true, 22 | suggestions: settings.translatorSuggestions, 23 | value: settings.altTextTranslator, 24 | size: 60, 25 | errors: settings.getErrors('altTextTranslator'), 26 | }) }} 27 | 28 | {{ forms.autosuggestField({ 29 | label: "HuggingFace API token"|t('altfy'), 30 | id: 'huggingFaceApiToken', 31 | name: 'huggingFaceApiToken', 32 | suggestEnvVars: true, 33 | value: settings.huggingFaceApiToken, 34 | size: 60, 35 | errors: settings.getErrors('huggingFaceApiToken') 36 | }) }} 37 | 38 | {{ forms.autosuggestField({ 39 | label: "DeepL API key"|t('altify'), 40 | id: 'deeplApiKey', 41 | name: 'deeplApiKey', 42 | suggestEnvVars: true, 43 | value: settings.deeplApiKey, 44 | size: 60, 45 | errors: settings.getErrors('deeplApiKey') 46 | }) }} 47 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fork/craft-altify", 3 | "description": "Generates alt texts for images using different services that can be chosen from.", 4 | "type": "craft-plugin", 5 | "license": "mit", 6 | "support": { 7 | "email": "obj@fork.de", 8 | "issues": "https://github.com/fork/craft-altify/issues?state=open", 9 | "source": "https://github.com/fork/craft-altify", 10 | "docs": "https://github.com/fork/craft-altify", 11 | "rss": "https://github.com/fork/craft-altify/releases.atom" 12 | }, 13 | "require": { 14 | "php": ">=8.0.2", 15 | "craftcms/cms": "^4.5.0", 16 | "deeplcom/deepl-php": "^1.7" 17 | }, 18 | "require-dev": { 19 | "craftcms/ecs": "dev-main", 20 | "craftcms/phpstan": "dev-main" 21 | }, 22 | "autoload": { 23 | "psr-4": { 24 | "fork\\altify\\": "src/" 25 | } 26 | }, 27 | "extra": { 28 | "handle": "altify", 29 | "name": "Altify", 30 | "developer": "Fork", 31 | "documentationUrl": "https://github.com/fork/craft-altify" 32 | }, 33 | "scripts": { 34 | "check-cs": "ecs check --ansi", 35 | "fix-cs": "ecs check --ansi --fix", 36 | "phpstan": "phpstan --memory-limit=1G" 37 | }, 38 | "config": { 39 | "sort-packages": true, 40 | "platform": { 41 | "php": "8.0.2" 42 | }, 43 | "allow-plugins": { 44 | "yiisoft/yii2-composer": true, 45 | "craftcms/plugin-installer": true, 46 | "php-http/discovery": true 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/icon-mask.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/connectors/alttextgeneration/AbstractHuggingFaceAltTextGenerator.php: -------------------------------------------------------------------------------- 1 | getVolume()->getFs(); 28 | if (!($fs instanceof Local)) { 29 | throw new NotALocalFileException("Image is not a local file"); 30 | } 31 | 32 | $fsPath = Craft::getAlias($fs->path); 33 | $absPath = $fsPath . DIRECTORY_SEPARATOR . $image->getPath(); 34 | 35 | $client = $this->getClient(); 36 | 37 | $body = Utils::tryFopen($absPath, 'r'); 38 | $response = $client->post( 39 | $this->modelPath, 40 | ['body' => $body] 41 | ); 42 | 43 | $body = $response->getBody(); 44 | $decoded = json_decode($body, true); 45 | $first = !empty($decoded) ? $decoded[0] : null; 46 | 47 | return $first ? $this->filterWords($first['generated_text']) : null; 48 | } 49 | 50 | protected function filterWords(string $text): string 51 | { 52 | foreach (Plugin::getInstance()->getSettings()->wordsBlackList as $word) { 53 | $text = str_replace($word, '', $text); 54 | } 55 | 56 | return trim($text); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/elements/actions/GenerateAltText.php: -------------------------------------------------------------------------------- 1 | getView()->registerJsWithVars(fn($type) => << { 26 | new Craft.ElementActionTrigger({ 27 | type: $type, 28 | 29 | // Whether this action should be available when multiple elements are selected 30 | bulk: true, 31 | 32 | // Return whether the action should be available depending on which elements are selected 33 | validateSelection: (selectedItems) { 34 | return true; 35 | }, 36 | 37 | // Uncomment if the action should be handled by JavaScript: 38 | // activate: () => { 39 | // Craft.elementIndex.setIndexBusy(); 40 | // const ids = Craft.elementIndex.getSelectedElementIds(); 41 | // // ... 42 | // Craft.elementIndex.setIndexAvailable(); 43 | // }, 44 | }); 45 | })(); 46 | JS, [static::class]); 47 | 48 | return null; 49 | } 50 | 51 | /** 52 | * @param Craft\elements\db\ElementQueryInterface $query 53 | * @return bool 54 | * @throws MissingComponentException 55 | */ 56 | public function performAction(Craft\elements\db\ElementQueryInterface $query): bool 57 | { 58 | $elements = $query->all(); 59 | foreach ($elements as $element) { 60 | try { 61 | Plugin::getInstance()->generator->generateAltTextForImage($element->id); 62 | } catch (Exception|Throwable $e) { 63 | Craft::$app->getSession()->setError($e->getMessage()); 64 | } 65 | } 66 | 67 | return true; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/elements/actions/TranslateAltText.php: -------------------------------------------------------------------------------- 1 | getView()->registerJsWithVars(fn($type) => << { 26 | new Craft.ElementActionTrigger({ 27 | type: $type, 28 | 29 | // Whether this action should be available when multiple elements are selected 30 | bulk: true, 31 | 32 | // Return whether the action should be available depending on which elements are selected 33 | validateSelection: (selectedItems) { 34 | return true; 35 | }, 36 | 37 | // Uncomment if the action should be handled by JavaScript: 38 | // activate: () => { 39 | // Craft.elementIndex.setIndexBusy(); 40 | // const ids = Craft.elementIndex.getSelectedElementIds(); 41 | // // ... 42 | // Craft.elementIndex.setIndexAvailable(); 43 | // }, 44 | }); 45 | })(); 46 | JS, [static::class]); 47 | 48 | return null; 49 | } 50 | 51 | /** 52 | * @param Craft\elements\db\ElementQueryInterface $query 53 | * @return bool 54 | * @throws MissingComponentException 55 | */ 56 | public function performAction(Craft\elements\db\ElementQueryInterface $query): bool 57 | { 58 | $elements = $query->all(); 59 | foreach ($elements as $element) { 60 | try { 61 | Plugin::getInstance()->translator->translateAltTextForImage($element->id); 62 | } catch (Exception|Throwable $e) { 63 | Craft::$app->getSession()->setError($e->getMessage()); 64 | } 65 | } 66 | 67 | return true; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/services/Generator.php: -------------------------------------------------------------------------------- 1 | elements->getElementById($assetId); 45 | AssetHelper::validateImage($image); 46 | /** @var Asset $image */ 47 | $image->alt = $this->generateAltText($image); 48 | 49 | if (!Craft::$app->elements->saveElement($image)) { 50 | throw new ImageNotSavedException( 51 | "Image could not be saved, reasons: " . json_encode($image->getErrors(), JSON_PRETTY_PRINT) 52 | ); 53 | } 54 | } 55 | 56 | /** 57 | * @throws InvalidConfigException 58 | */ 59 | public function generateAltText(Asset $image): string 60 | { 61 | return Plugin::getInstance()->getSettings()->getAltTextGenerator()->generateAltTextForImage($image); 62 | } 63 | 64 | /** 65 | * @return array 66 | */ 67 | public function getAvailableGenerators(): array 68 | { 69 | $generators = self::buildConnectorArray(self::GENERATORS); 70 | $registerGeneratorsEvent = new RegisterGeneratorsEvent(['generators' => $generators]); 71 | $this->trigger(self::EVENT_REGISTER_GENERATORS, $registerGeneratorsEvent); 72 | 73 | return $registerGeneratorsEvent->generators; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/services/Translator.php: -------------------------------------------------------------------------------- 1 | elements->getElementById($assetId); 46 | AssetHelper::validateImage($image); 47 | /** @var Asset $image */ 48 | $image->alt = $this->translateAltText($image); 49 | 50 | if (!Craft::$app->elements->saveElement($image)) { 51 | throw new ImageNotSavedException( 52 | "Image could not be saved, reasons: " . json_encode($image->getErrors(), JSON_PRETTY_PRINT) 53 | ); 54 | } 55 | } 56 | 57 | /** 58 | * @throws InvalidConfigException 59 | */ 60 | public function translateAltText(Asset $image): string 61 | { 62 | return Plugin::getInstance()->getSettings()->getAltTextTranslator()->translateAltTextForImage($image); 63 | } 64 | 65 | /** 66 | * @return array 67 | */ 68 | public function getAvailableTranslators(): array 69 | { 70 | $translators = self::buildConnectorArray(self::TRANSLATORS); 71 | $registerTranslatorsEvent = new RegisterTranslatorsEvent(['translators' => $translators]); 72 | $this->trigger(self::EVENT_REGISTER_TRANSLATORS, $registerTranslatorsEvent); 73 | 74 | return $registerTranslatorsEvent->translators; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/models/Settings.php: -------------------------------------------------------------------------------- 1 | huggingFaceApiToken); 37 | } 38 | 39 | public function getDeeplApiKey(): ?string 40 | { 41 | return App::parseEnv($this->deeplApiKey); 42 | } 43 | 44 | /** 45 | * @return array[] 46 | * @noinspection PhpUnused 47 | */ 48 | public function getGeneratorSuggestions(): array 49 | { 50 | $data = []; 51 | 52 | foreach (Plugin::getInstance()->generator->getAvailableGenerators() as $handle => $classname) { 53 | /** @var AltTextGeneratorInterface $obj */ 54 | $obj = new $classname(); 55 | 56 | $data[] = [ 57 | 'name' => $handle, 58 | 'hint' => $obj->getName() 59 | ]; 60 | } 61 | 62 | return [[ 63 | 'label' => Craft::t('altify', 'Generators'), 64 | 'data' => $data 65 | ]]; 66 | } 67 | 68 | /** 69 | * @return array[] 70 | * @noinspection PhpUnused 71 | */ 72 | public function getTranslatorSuggestions(): array 73 | { 74 | $data = []; 75 | 76 | foreach (Plugin::getInstance()->translator->getAvailableTranslators() as $handle => $classname) { 77 | /** @var TranslatorInterface $obj */ 78 | $obj = new $classname(); 79 | 80 | $data[] = [ 81 | 'name' => $handle, 82 | 'hint' => $obj->getName() 83 | ]; 84 | } 85 | 86 | return [[ 87 | 'label' => Craft::t('altify', 'Translators'), 88 | 'data' => $data 89 | ]]; 90 | } 91 | 92 | /** 93 | * @throws InvalidConfigException 94 | */ 95 | public function getAltTextGenerator(): AltTextGeneratorInterface 96 | { 97 | $altTextGenerator = App::parseEnv($this->altTextGenerator); 98 | $generators = Plugin::getInstance()->generator->getAvailableGenerators(); 99 | 100 | if (key_exists($altTextGenerator, $generators)) { 101 | $className = $generators[$altTextGenerator]; 102 | } else { 103 | $className = HuggingFaceBlipLargeAltTextGenerator::class; 104 | } 105 | if (!is_a($className, AltTextGeneratorInterface::class, true)) { 106 | throw new InvalidConfigException(Craft::t( 107 | 'altify', 108 | '{class} must implement {interface}', 109 | [ 110 | 'class' => Plugin::getInstance()->getSettings()->altTextGenerator, 111 | 'interface' => AltTextGeneratorInterface::class 112 | ] 113 | )); 114 | } 115 | 116 | return new $className; 117 | } 118 | 119 | /** 120 | * @throws InvalidConfigException 121 | */ 122 | public function getAltTextTranslator(): TranslatorInterface 123 | { 124 | $altTextTranslator = App::parseEnv($this->altTextTranslator); 125 | $translators = Plugin::getInstance()->translator->getAvailableTranslators(); 126 | 127 | if (key_exists($altTextTranslator, $translators)) { 128 | $className = $translators[$altTextTranslator]; 129 | } else { 130 | $className = HuggingFaceT5SmallTranslator::class; 131 | } 132 | if (!is_a($className, TranslatorInterface::class, true)) { 133 | throw new InvalidConfigException(Craft::t( 134 | 'altify', 135 | '{class} must implement {interface}', 136 | [ 137 | 'class' => Plugin::getInstance()->getSettings()->altTextTranslator, 138 | 'interface' => TranslatorInterface::class 139 | ] 140 | )); 141 | } 142 | 143 | return new $className; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/Plugin.php: -------------------------------------------------------------------------------- 1 | 33 | * @copyright Fork 34 | * @license MIT 35 | * @property-read Generator $generator 36 | * @property-read Settings $settings 37 | * @property-read Translator $translator 38 | */ 39 | class Plugin extends BasePlugin 40 | { 41 | public ?string $name = 'Altify'; 42 | public string $schemaVersion = '1.0.0'; 43 | public bool $hasCpSettings = true; 44 | 45 | public static function config(): array 46 | { 47 | return [ 48 | 'components' => [ 49 | 'generator' => Generator::class, 50 | 'translator' => Translator::class 51 | ], 52 | ]; 53 | } 54 | 55 | public function init(): void 56 | { 57 | parent::init(); 58 | 59 | // Defer most setup tasks until Craft is fully initialized 60 | Craft::$app->onInit(function() { 61 | $this->attachEventHandlers(); 62 | }); 63 | } 64 | 65 | /** 66 | * @throws InvalidConfigException 67 | */ 68 | protected function createSettingsModel(): ?Model 69 | { 70 | return Craft::createObject(Settings::class); 71 | } 72 | 73 | /** 74 | * @throws SyntaxError 75 | * @throws Exception 76 | * @throws RuntimeError 77 | * @throws LoaderError 78 | */ 79 | protected function settingsHtml(): ?string 80 | { 81 | $settings = $this->getSettings(); 82 | 83 | return Craft::$app->view->renderTemplate('altify/_settings.twig', [ 84 | 'plugin' => $this, 85 | 'settings' => $settings 86 | ]); 87 | } 88 | 89 | private function attachEventHandlers(): void 90 | { 91 | Event::on( 92 | Asset::class, 93 | Element::EVENT_AFTER_SAVE, 94 | function (ModelEvent $event) { 95 | $asset = $event->sender; 96 | 97 | if ($asset->firstSave && $asset->kind === Asset::KIND_IMAGE) { 98 | Craft::$app->getQueue()->push(new GenerateAltTextJob(['assetId' => $asset->id])); 99 | } 100 | } 101 | ); 102 | 103 | Event::on( 104 | Asset::class, 105 | Element::EVENT_DEFINE_ADDITIONAL_BUTTONS, 106 | function (DefineHtmlEvent $event) { 107 | $this->appendAssetEditPageButtons($event); 108 | } 109 | ); 110 | 111 | Event::on( 112 | Asset::class, 113 | Element::EVENT_REGISTER_ACTIONS, 114 | function (RegisterElementActionsEvent $event) { 115 | $event->actions[] = GenerateAltTextAction::class; 116 | $event->actions[] = TranslateAltTextAction::class; 117 | } 118 | ); 119 | } 120 | 121 | /** 122 | * @param DefineHtmlEvent $event 123 | * @return void 124 | */ 125 | private function appendAssetEditPageButtons(DefineHtmlEvent &$event): void 126 | { 127 | /** @see Asset::getAdditionalButtons() */ 128 | $event->html = Html::beginTag('div', ['class' => 'btngroup']); 129 | $event->html .= Html::button(Craft::t('altify', 'Generate alt text'), [ 130 | 'id' => 'generateAltText-btn', 131 | 'class' => 'btn', 132 | 'data' => [ 133 | 'icon' => 'wand', 134 | ], 135 | 'aria' => [ 136 | 'label' => Craft::t('altify', 'Generate alt text'), 137 | ], 138 | ]); 139 | $event->html .= Html::button(Craft::t('altify', 'Translate alt text'), [ 140 | 'id' => 'translateAltText-btn', 141 | 'class' => 'btn', 142 | 'data' => [ 143 | 'icon' => 'language', 144 | ], 145 | 'aria' => [ 146 | 'label' => Craft::t('altify', 'Translate alt text'), 147 | ], 148 | ]); 149 | $js = << { 151 | let id = document.querySelector("input[name='elementId']").value; 152 | const \$form = Craft.createForm().appendTo(Garnish.\$bod); 153 | \$form.append(Craft.getCsrfInput()); 154 | $('', {type: 'hidden', name: 'action', value: 'altify/generate-alt-text'}).appendTo(\$form); 155 | $('', {type: 'hidden', name: 'assetId', value: id}).appendTo(\$form); 156 | $('', {type: 'submit', value: 'Submit'}).appendTo(\$form); 157 | \$form.submit(); 158 | \$form.remove(); 159 | }); 160 | $('#translateAltText-btn').on('click', () => { 161 | let id = document.querySelector("input[name='elementId']").value; 162 | const \$form = Craft.createForm().appendTo(Garnish.\$bod); 163 | \$form.append(Craft.getCsrfInput()); 164 | $('', {type: 'hidden', name: 'action', value: 'altify/translate-alt-text'}).appendTo(\$form); 165 | $('', {type: 'hidden', name: 'assetId', value: id}).appendTo(\$form); 166 | $('', {type: 'submit', value: 'Submit'}).appendTo(\$form); 167 | \$form.submit(); 168 | \$form.remove(); 169 | }); 170 | JS; 171 | Craft::$app->getView()->registerJs($js); 172 | 173 | $event->html .= Html::endTag('div'); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | --------------------------------------------------------------------------------