Skip to content

Commit d3957ca

Browse files
authored
Merge pull request #12 from heavymetalavo/hotfix/asset-handling
hotfix/asset-handling
2 parents 76fb6ac + 0cb92c0 commit d3957ca

File tree

6 files changed

+68
-60
lines changed

6 files changed

+68
-60
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Release Notes for AI Alt Text
22

3+
## 1.5.3 - 2025-05-09
4+
- Improve bulk action notice wording
5+
- Improved logic to not skip generating alt text for an asset where a job is in the queue but it has a failed status
6+
- Replacing `preSaveAsset` setting with `propagate` setting, `preSaveAsset` tried to resolve an issue where the same value could be saved over multiple sites. Could sometimes cause errors e.g. `Failed to pre-save asset: filename.png`,
7+
- Replacing native `file_get_contents` function with `$assets->getContents` in animated gif test, which is more reliable across asset different platforms
8+
- Removing tests to check an asset's file extension which is not a reliable way to ascertain if the file will be accepted by the OpenAI API
9+
- Updating tests to check an asset's mime type to ascertain if an image transform to a different format is required before it is sent to OpenAI API
10+
- Added new test to check if resulting transform which will be sent to OpenAI is accepted mime type
11+
- Added new test to check if SVGs can be transformed to an accepted mime type
12+
313
## 1.5.2 - 2025-05-06
414

515
- Updating changelog formatting slightly to test supporting Craft's `Utilities → Updates` screen

src/controllers/GenerateController.php

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -177,17 +177,15 @@ public function actionGenerateAssetsWithoutAltText(): Response
177177
if ($siteId) {
178178
$siteName = $sites[0]->name;
179179
Craft::$app->getSession()->setNotice(
180-
Craft::t('ai-alt-text', 'Queued alt text generation for {count} assets in site {site} (out of {total} total assets without alt text).', [
180+
Craft::t('ai-alt-text', 'Queued alt text generation for {count} assets in site {site}', [
181181
'count' => $queuedCount,
182-
'total' => $totalCount,
183182
'site' => $siteName
184183
])
185184
);
186185
} else {
187186
Craft::$app->getSession()->setNotice(
188-
Craft::t('ai-alt-text', 'Queued alt text generation for {count} assets across all sites (out of {total} total assets without alt text).', [
187+
Craft::t('ai-alt-text', 'Queued alt text generation for {count} assets across all sites', [
189188
'count' => $queuedCount,
190-
'total' => $totalCount
191189
])
192190
);
193191
}
@@ -306,17 +304,15 @@ public function actionGenerateAllAssets(): Response
306304
if ($siteId) {
307305
$siteName = $sites[0]->name;
308306
Craft::$app->getSession()->setNotice(
309-
Craft::t('ai-alt-text', 'Queued alt text generation for {count} assets in site {site} (out of {total} total image assets).', [
307+
Craft::t('ai-alt-text', 'Queued alt text generation for {count} assets in site {site}.', [
310308
'count' => $queuedCount,
311-
'total' => $totalCount,
312309
'site' => $siteName
313310
])
314311
);
315312
} else {
316313
Craft::$app->getSession()->setNotice(
317-
Craft::t('ai-alt-text', 'Queued alt text generation for {count} assets across all sites (out of {total} total image assets).', [
314+
Craft::t('ai-alt-text', 'Queued alt text generation for {count} assets across all sites.', [
318315
'count' => $queuedCount,
319-
'total' => $totalCount
320316
])
321317
);
322318
}

src/models/Settings.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,9 @@ class Settings extends Model
4848
public string $openAiImageInputDetailLevel = 'low';
4949

5050
/**
51-
* @var bool Whether to pre-save the asset if alt field is empty before saving a value to it, prevents same value being saved to each Site
51+
* @var bool Whether the asset should be saved across all of its supported sites, if enabled it could save the same initial alt text value across all sites.
5252
*/
53-
public bool $preSaveAsset = true;
53+
public bool $propagate = false;
5454

5555
/**
5656
* @var bool Whether to save the translated result to each Site's Asset's translatable alt text field
@@ -74,7 +74,7 @@ public function defineRules(): array
7474
['prompt', 'string'],
7575
['openAiImageInputDetailLevel', 'string'],
7676
['openAiImageInputDetailLevel', 'in', 'range' => ['low', 'high']],
77-
['preSaveAsset', 'boolean'],
77+
['propagate', 'boolean'],
7878
['saveTranslatedResultsToEachSite', 'boolean'],
7979
['generateForNewAssets', 'boolean'],
8080
];

src/services/AiAltTextService.php

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,10 @@ public function createJob(Asset $asset, $saveCurrentSiteOffQueue = false, $curre
5555
$hasExistingJob = false;
5656
foreach ($existingJobs as $job) {
5757
// Only skip if both asset ID AND site ID match an existing job
58-
if (isset($job['description']) &&
59-
str_contains($job['description'], "Asset: $asset->id") &&
60-
str_contains($job['description'], "Site: $assetSiteId")) {
58+
if (isset($job['description'])
59+
&& str_contains($job['description'], "Asset: $asset->id")
60+
&& str_contains($job['description'], "Site: $assetSiteId")
61+
&& $job['status'] !== 4) {
6162
$hasExistingJob = true;
6263
break;
6364
}
@@ -143,23 +144,15 @@ public function generateAltText(Asset $asset, int $siteId = null, bool $forceReg
143144
throw new Exception('Asset must be an image');
144145
}
145146

146-
// Only pre-save if the alt is empty and we're not forcing regeneration
147-
if (AiAltText::getInstance()->getSettings()->preSaveAsset &&
148-
(empty($asset->alt) || $forceRegeneration)) {
149-
$asset->alt = '';
150-
if (!Craft::$app->elements->saveElement($asset)) {
151-
throw new Exception('Failed to pre-save asset: ' . $asset->filename);
152-
}
153-
}
154-
155147
$altText = $this->openAiService->generateAltText($asset, $siteId);
156148

157149
if (empty($altText)) {
158150
throw new Exception('Empty alt text generated for asset: ' . $asset->filename);
159151
}
160152

161153
$asset->alt = $altText;
162-
if (!Craft::$app->elements->saveElement($asset, true)) {
154+
$propagate = AiAltText::getInstance()->getSettings()->propagate;
155+
if (!Craft::$app->elements->saveElement($asset, true, $propagate)) {
163156
throw new Exception('Failed to save alt text for asset: ' . $asset->filename);
164157
}
165158

src/services/OpenAiService.php

Lines changed: 39 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -154,21 +154,13 @@ private function isUrlAccessible(string $url): bool
154154
* Validates if the asset is an accepted image format and converts if needed
155155
*
156156
* @param Asset $asset The asset to validate
157-
* @return bool Whether the asset is an accepted format or was successfully converted
157+
* @return bool Whether the asset is an accepted mimetype or was successfully converted
158158
* @throws ImageTransformException
159159
*/
160160
private function isValidImageFormat(Asset $asset): bool
161161
{
162-
$extension = strtolower($asset->getExtension());
163162
$mimeType = strtolower($asset->getMimeType());
164163

165-
// Check for accepted extensions
166-
$acceptedExtensions = ['png', 'jpg', 'jpeg', 'webp', 'gif'];
167-
if (!in_array($extension, $acceptedExtensions)) {
168-
Craft::warning('Asset has unsupported extension: ' . $extension . '. Will attempt to convert to JPEG.', __METHOD__);
169-
return true; // We'll handle conversion in generateAltText
170-
}
171-
172164
// Check for accepted MIME types
173165
$acceptedMimeTypes = [
174166
'image/png',
@@ -184,14 +176,10 @@ private function isValidImageFormat(Asset $asset): bool
184176

185177
// For GIFs, check if they're animated
186178
if ($extension === 'gif' && $mimeType === 'image/gif') {
187-
$filePath = $asset->getPath();
188-
if (file_exists($filePath)) {
189-
$fileContents = file_get_contents($filePath);
190-
// Check for multiple image frames in GIF
191-
if (substr_count($fileContents, "\x21\xF9\x04") > 1) {
192-
Craft::warning('Animated GIF detected. Will attempt to convert first frame to JPEG.', __METHOD__);
193-
return true; // We'll handle conversion in generateAltText
194-
}
179+
$fileContents = $asset->getContents();
180+
// Check for multiple image frames in GIF
181+
if (substr_count($fileContents, "\x21\xF9\x04") > 1) {
182+
return false;
195183
}
196184
}
197185

@@ -228,9 +216,30 @@ public function generateAltText(Asset $asset, int $siteId = null): string
228216
$height = $asset->getHeight();
229217

230218
// Check if format needs to be converted
231-
$extension = strtolower($asset->getExtension());
232-
$acceptedExtensions = ['png', 'jpg', 'jpeg', 'webp', 'gif'];
233-
$needsFormatConversion = !in_array($extension, $acceptedExtensions);
219+
$acceptedMimeTypes = [
220+
'image/png',
221+
'image/jpeg',
222+
'image/webp',
223+
'image/gif',
224+
];
225+
$assetMimeType = strtolower($asset->getMimeType());
226+
227+
// Check if the asset is an animated gif which OpenAI API does not support
228+
if ($assetMimeType === 'image/gif') {
229+
$fileContents = $asset->getContents();
230+
// Check for multiple image frames in GIF
231+
if (substr_count($fileContents, "\x21\xF9\x04") > 1) {
232+
throw new Exception('Animated GIF detected, this is not supported.');
233+
}
234+
}
235+
236+
// decide if we need to transform svgs
237+
if (!Craft::$app->getConfig()->getGeneral()->transformSvgs && $assetMimeType === 'image/svg+xml') {
238+
throw new Exception('SVGs are not supported by the OpenAI API and transformSvgs is disabled.');
239+
}
240+
241+
// decide if we need to transform the image to become a jpeg
242+
$needsFormatConversion = !in_array($assetMimeType, $acceptedMimeTypes);
234243

235244
// Set up transform parameters
236245
$transformParams = [];
@@ -247,9 +256,15 @@ public function generateAltText(Asset $asset, int $siteId = null): string
247256
$transformParams['mode'] = 'fit';
248257
}
249258

250-
// Get the image URL with transformation if needed
259+
// Check mime type of the transform:
260+
$transformMimeType = $asset->getMimeType($transformParams);
261+
if (!in_array($transformMimeType, $acceptedMimeTypes)) {
262+
throw new Exception("Asset transform has unsupported MIME type: $transformMimeType");
263+
}
264+
265+
$assetTransform = $asset->setTransform($transformParams);
251266
// Make sure that we do not get a "generate transform" url, but a real url with true
252-
$imageUrl = $asset->getUrl($transformParams, true);
267+
$imageUrl = $assetTransform->getUrl($transformParams, true);
253268

254269
// If we have a URL, check if it's accessible remotely
255270
if (!empty($imageUrl)) {
@@ -261,17 +276,11 @@ public function generateAltText(Asset $asset, int $siteId = null): string
261276

262277
// If no public URL is available or URL is not accessible, try to get the file contents and encode as base64
263278
if (empty($imageUrl) || !$asset->getVolume()->getFs()->hasUrls) {
264-
$assetContents = $asset->getContents();
265-
266-
// Get the MIME type
267-
$mimeType = $asset->getMimeType();
268-
if (empty($mimeType)) {
269-
$mimeType = 'image/jpeg'; // Default to JPEG if MIME type is unknown
270-
}
279+
$assetContents = $assetTransform->getContents();
271280

272281
// Encode as base64 and create data URI
273282
$base64Image = base64_encode($assetContents);
274-
$imageUrl = "data:$mimeType;base64,$base64Image";
283+
$imageUrl = "data:$transformMimeType;base64,$base64Image";
275284
}
276285

277286
$detail = App::parseEnv(AiAltText::getInstance()->getSettings()->openAiImageInputDetailLevel) ?? 'low';

src/templates/_settings.twig

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -117,12 +117,12 @@
117117
}) }}
118118

119119
{{ forms.lightswitchField({
120-
label: 'Pre-save Asset'|t('aialttext'),
121-
id: 'preSaveAsset',
122-
name: 'preSaveAsset',
123-
instructions: "If enabled, the plugin will pre-save the asset if alt field is empty before saving a value to it, prevents same initial value being saved to each Site"|t("aialttext"),
124-
on: settings.preSaveAsset,
125-
errors: settings.getErrors('preSaveAsset'),
120+
label: 'propagate'|t('aialttext'),
121+
id: 'propagate',
122+
name: ' ',
123+
instructions: "Whether the asset should be saved across all of its supported sites, if enabled it could save the same initial alt text value across all sites."|t("aialttext"),
124+
on: settings.propagate,
125+
errors: settings.getErrors('propagate'),
126126
}) }}
127127

128128
{{ forms.lightswitchField({

0 commit comments

Comments
 (0)