diff --git a/administrator/language/en-GB/plg_media-action_crop.ini b/administrator/language/en-GB/plg_media-action_crop.ini
index 57c9846ad67a0..9c5c13dd7d82c 100644
--- a/administrator/language/en-GB/plg_media-action_crop.ini
+++ b/administrator/language/en-GB/plg_media-action_crop.ini
@@ -4,6 +4,8 @@
; Note : All ini files need to be saved as UTF-8
PLG_MEDIA-ACTION_CROP="Media Action - Crop"
+PLG_MEDIA-ACTION_CROP_ASPECT_RATIOS_DESC="Configure custom aspect ratios for the crop tool. Each ratio needs a label (e.g. '16:9'), a value (e.g. '16/9' or '1'), and optionally a group (landscape/portrait)."
+PLG_MEDIA-ACTION_CROP_ASPECT_RATIOS_LABEL="Aspect Ratios"
PLG_MEDIA-ACTION_CROP_LABEL="Crop"
PLG_MEDIA-ACTION_CROP_PARAM_ASPECT="Aspect Ratio"
PLG_MEDIA-ACTION_CROP_PARAM_DEFAULT_RATIO="Default aspect ratio"
@@ -15,4 +17,11 @@ PLG_MEDIA-ACTION_CROP_PARAM_WIDTH="Width"
PLG_MEDIA-ACTION_CROP_PARAM_X="X-Axis"
PLG_MEDIA-ACTION_CROP_PARAM_Y="Y-Axis"
PLG_MEDIA-ACTION_CROP_QUALITY="Quality"
+PLG_MEDIA-ACTION_CROP_RATIO_GROUP_LABEL="Group"
+PLG_MEDIA-ACTION_CROP_RATIO_GROUP_DESC="Optionally group this ratio under landscape or portrait"
+PLG_MEDIA-ACTION_CROP_RATIO_GROUP_NONE="No group"
+PLG_MEDIA-ACTION_CROP_RATIO_LABEL_LABEL="Label"
+PLG_MEDIA-ACTION_CROP_RATIO_LABEL_DESC="The label to display for this ratio (e.g. '16:9', '4:3', 'thumbnail', etc.)"
+PLG_MEDIA-ACTION_CROP_RATIO_VALUE_LABEL="Value"
+PLG_MEDIA-ACTION_CROP_RATIO_VALUE_DESC="The aspect ratio as numerator/denominator or number (e.g. 16/9, 4/3, 1)."
PLG_MEDIA-ACTION_CROP_XML_DESCRIPTION="Adds crop functionality for images."
diff --git a/build/media_source/plg_media-action_crop/js/crop.es6.js b/build/media_source/plg_media-action_crop/js/crop.es6.js
index ddeb2649181d3..68a9c3be071d6 100644
--- a/build/media_source/plg_media-action_crop/js/crop.es6.js
+++ b/build/media_source/plg_media-action_crop/js/crop.es6.js
@@ -8,6 +8,23 @@ let formElements;
let activated = false;
let instance;
+/**
+ * Parse aspect ratio value from string format to number
+ * Accepts either a plain number (e.g. "1") or numerator/denominator format (e.g. "16/9")
+ * @param {string} value - The aspect ratio value
+ * @returns {number} The calculated aspect ratio as a number
+ */
+const parseAspectRatio = (value) => {
+ if (typeof value !== 'string' || !value.trim()) {
+ return NaN;
+ }
+ if (value.includes('/')) {
+ const [numerator, denominator] = value.split('/').map(parseFloat);
+ return isNaN(numerator) || isNaN(denominator) || denominator === 0 ? NaN : numerator / denominator;
+ }
+ return parseFloat(value);
+};
+
const addListeners = () => {
formElements.cropX.addEventListener('change', ({ currentTarget }) => {
instance.setData({ x: parseInt(currentTarget.value, 10) });
@@ -22,7 +39,7 @@ const addListeners = () => {
instance.setData({ height: parseInt(currentTarget.value, 10) });
});
formElements.aspectRatio.addEventListener('change', ({ currentTarget }) => {
- instance.setAspectRatio(currentTarget.value);
+ instance.setAspectRatio(parseAspectRatio(currentTarget.value));
});
activated = true;
};
@@ -66,7 +83,7 @@ const init = (image) => {
addListeners();
}
- instance.setAspectRatio(formElements.cropAspectRatioOption.value);
+ instance.setAspectRatio(parseAspectRatio(formElements.cropAspectRatioOption.value));
};
// Register the Events
diff --git a/plugins/media-action/crop/crop.xml b/plugins/media-action/crop/crop.xml
index 4b914e679a0aa..88708674ab74b 100644
--- a/plugins/media-action/crop/crop.xml
+++ b/plugins/media-action/crop/crop.xml
@@ -19,4 +19,54 @@
language/en-GB/plg_media-action_crop.ini
language/en-GB/plg_media-action_crop.sys.ini
+
+
+
+
+
diff --git a/plugins/media-action/crop/form/crop.xml b/plugins/media-action/crop/form/crop.xml
index 696bde1336c20..e2c8daee7f4ef 100644
--- a/plugins/media-action/crop/form/crop.xml
+++ b/plugins/media-action/crop/form/crop.xml
@@ -64,22 +64,22 @@
label="PLG_MEDIA-ACTION_CROP_PARAM_ASPECT"
hiddenLabel="true"
class="crop-aspect-ratio-options"
- default="1.111"
- >
-
+ default="10/9"
+ >
+
-
-
-
-
+
+
+
+
-
-
-
-
+
+
+
+
diff --git a/plugins/media-action/crop/src/Extension/Crop.php b/plugins/media-action/crop/src/Extension/Crop.php
index 8f56d62876889..1ef09b18c8904 100644
--- a/plugins/media-action/crop/src/Extension/Crop.php
+++ b/plugins/media-action/crop/src/Extension/Crop.php
@@ -11,6 +11,7 @@
namespace Joomla\Plugin\MediaAction\Crop\Extension;
use Joomla\CMS\Application\CMSWebApplicationInterface;
+use Joomla\CMS\Form\Form;
use Joomla\Component\Media\Administrator\Plugin\MediaActionPlugin;
use Joomla\Event\SubscriberInterface;
@@ -25,6 +26,159 @@
*/
final class Crop extends MediaActionPlugin implements SubscriberInterface
{
+ /**
+ * The form event. Load additional parameters when available into the field form.
+ * Override to dynamically inject aspect ratios from plugin settings.
+ *
+ * @param Form $form The form
+ * @param \stdClass $data The data (required by the event interface, not used in this implementation)
+ *
+ * @return void
+ *
+ * @since __DEPLOY_VERSION__
+ * @note The $data parameter is required by the Joomla event interface but is not used in this method.
+ */
+ public function onContentPrepareForm(Form $form, $data): void
+ {
+ // Check if it is the right form
+ if ($form->getName() !== 'com_media.file') {
+ return;
+ }
+
+ $this->loadCss();
+ $this->loadJs();
+
+ // The file with the params for the edit view
+ $paramsFile = JPATH_PLUGINS . '/media-action/' . $this->_name . '/form/' . $this->_name . '.xml';
+
+ // When the file exists, load it into the form
+ if (file_exists($paramsFile)) {
+ $form->loadFile($paramsFile);
+
+ // Get the aspect ratios from plugin parameters
+ $aspectRatios = $this->params->get('aspect_ratios');
+
+ // Convert to array if needed (handles stdClass from Registry)
+ if (\is_object($aspectRatios)) {
+ $aspectRatios = (array) $aspectRatios;
+ }
+
+ // If we have custom aspect ratios, modify the form field
+ if (!empty($aspectRatios) && \is_array($aspectRatios)) {
+ $this->injectAspectRatios($form, $aspectRatios);
+ }
+ }
+ }
+
+ /**
+ * Inject custom aspect ratios into the crop form
+ *
+ * @param Form $form The form object
+ * @param array $aspectRatios The aspect ratios from plugin settings
+ *
+ * @return void
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ private function injectAspectRatios(Form $form, array $aspectRatios): void
+ {
+ // Get the aspectRatio field (try without group first, then with 'crop' group)
+ $field = $form->getField('aspectRatio');
+
+ if (!$field) {
+ $field = $form->getField('aspectRatio', 'crop');
+ }
+
+ if (!$field) {
+ return;
+ }
+
+ // Build new XML for the field with custom options
+ $xml = new \SimpleXMLElement('');
+ $xml->addAttribute('name', 'aspectRatio');
+ $xml->addAttribute('type', 'groupedlist');
+ $xml->addAttribute('label', 'PLG_MEDIA-ACTION_CROP_PARAM_ASPECT');
+ $xml->addAttribute('hiddenLabel', 'true');
+ $xml->addAttribute('class', 'crop-aspect-ratio-options');
+ $xml->addAttribute('default', '10/9');
+
+ // Add default options
+ $option = $xml->addChild('option', 'PLG_MEDIA-ACTION_CROP_PARAM_DEFAULT_RATIO');
+ $option->addAttribute('class', 'crop-aspect-ratio-option');
+ $option->addAttribute('value', '10/9');
+
+ $option = $xml->addChild('option', 'PLG_MEDIA-ACTION_CROP_PARAM_NO_RATIO');
+ $option->addAttribute('class', 'crop-aspect-ratio-option');
+ $option->addAttribute('value', '');
+
+ // Group ratios by landscape/portrait
+ $grouped = [
+ '' => [],
+ 'landscape' => [],
+ 'portrait' => [],
+ ];
+
+ foreach ($aspectRatios as $ratio) {
+ // Convert individual ratio to array if it's an object
+ $ratio = (array) $ratio;
+
+ $label = $ratio['label'] ?? '';
+ $value = $ratio['value'] ?? '';
+ $group = strtolower(trim($ratio['group'] ?? ''));
+
+ if (empty($label) || $value === '' || $value === null) {
+ continue;
+ }
+
+ if (!isset($grouped[$group])) {
+ $grouped[$group] = [];
+ }
+
+ $grouped[$group][] = [
+ 'label' => $label,
+ 'value' => $value,
+ 'group' => $group,
+ ];
+ }
+
+ // Add ungrouped ratios
+ foreach ($grouped[''] as $ratio) {
+ $option = $xml->addChild('option', htmlspecialchars($ratio['label'], ENT_XML1, 'UTF-8'));
+ $option->addAttribute('class', 'crop-aspect-ratio-option');
+ $option->addAttribute('value', htmlspecialchars($ratio['value'], ENT_XML1, 'UTF-8'));
+ }
+
+ // Add landscape group
+ if (!empty($grouped['landscape'])) {
+ $group = $xml->addChild('group');
+ $group->addAttribute('label', 'PLG_MEDIA-ACTION_CROP_PARAM_LANDSCAPE');
+
+ foreach ($grouped['landscape'] as $ratio) {
+ $option = $group->addChild('option', htmlspecialchars($ratio['label'], ENT_XML1, 'UTF-8'));
+ $option->addAttribute('class', 'crop-aspect-ratio-option');
+ $option->addAttribute('value', htmlspecialchars($ratio['value'], ENT_XML1, 'UTF-8'));
+ }
+ }
+
+ // Add portrait group
+ if (!empty($grouped['portrait'])) {
+ $group = $xml->addChild('group');
+ $group->addAttribute('label', 'PLG_MEDIA-ACTION_CROP_PARAM_PORTRAIT');
+
+ foreach ($grouped['portrait'] as $ratio) {
+ $option = $group->addChild('option', htmlspecialchars($ratio['label'], ENT_XML1, 'UTF-8'));
+ $option->addAttribute('class', 'crop-aspect-ratio-option');
+ $option->addAttribute('value', htmlspecialchars($ratio['value'], ENT_XML1, 'UTF-8'));
+ }
+ }
+
+ // Replace the field in the form
+ // Try setting in default group first, then 'crop' group
+ if (!$form->setField($xml, null, true)) {
+ $form->setField($xml, 'crop', true);
+ }
+ }
+
/**
* Load the javascript files of the plugin.
*
@@ -32,7 +186,7 @@ final class Crop extends MediaActionPlugin implements SubscriberInterface
*
* @since 4.0.0
*/
- protected function loadJs()
+ protected function loadJs(): void
{
parent::loadJs();
@@ -50,7 +204,7 @@ protected function loadJs()
*
* @since 4.0.0
*/
- protected function loadCss()
+ protected function loadCss(): void
{
parent::loadCss();