diff --git a/CRM/Mosaico/Form/MosaicoAdmin.php b/CRM/Mosaico/Form/MosaicoAdmin.php index 18b348cea9..754e2ff6cd 100644 --- a/CRM/Mosaico/Form/MosaicoAdmin.php +++ b/CRM/Mosaico/Form/MosaicoAdmin.php @@ -12,6 +12,10 @@ class CRM_Mosaico_Form_MosaicoAdmin extends CRM_Admin_Form_Setting { protected $_settings = [ 'mosaico_layout' => 'Mosaico Preferences', 'mosaico_graphics' => 'Mosaico Preferences', + 'mosaico_scale_factor1' => 'Mosaico Preferences', + 'mosaico_scale_factor2' => 'Mosaico Preferences', + 'mosaico_scale_width_limit1' => 'Mosaico Preferences', + 'mosaico_scale_width_limit2' => 'Mosaico Preferences', 'mosaico_custom_templates_dir' => 'Mosaico Custom Templates Directory', 'mosaico_custom_templates_url' => 'Mosaico Custom Templates URL' ]; diff --git a/CRM/Mosaico/Graphics/Imagick.php b/CRM/Mosaico/Graphics/Imagick.php index b31792fea8..d91e6049d8 100644 --- a/CRM/Mosaico/Graphics/Imagick.php +++ b/CRM/Mosaico/Graphics/Imagick.php @@ -10,7 +10,7 @@ * * @see https://github.com/voidlabs/mosaico/blob/master/backend/README.txt */ -class CRM_Mosaico_Graphics_Imagick implements CRM_Mosaico_Graphics_Interface { +class CRM_Mosaico_Graphics_Imagick extends CRM_Mosaico_Graphics_Interface { /** * CRM_Mosaico_Graphics_Imagick constructor. @@ -79,6 +79,7 @@ public function createResizedImage($srcFile, $destFile, $width, $height) { $mobileMinWidth = $config['MOBILE_MIN_WIDTH']; $image = new Imagick($srcFile); + $this->adjustResizeDimensions($image->getImageWidth(), $image->getImageHeight(), $width, $height); $resize_width = $width; $resize_height = $image->getImageHeight(); @@ -109,6 +110,7 @@ public function createCoveredImage($srcFile, $destFile, $width, $height) { $image = new Imagick($srcFile); $image_geometry = $image->getImageGeometry(); + $this->adjustResizeDimensions($image_geometry["width"], $image_geometry["height"], $width, $height); $width_ratio = $image_geometry["width"] / $width; $height_ratio = $image_geometry["height"] / $height; diff --git a/CRM/Mosaico/Graphics/Interface.php b/CRM/Mosaico/Graphics/Interface.php index f93e8fc8de..f1296e7e65 100644 --- a/CRM/Mosaico/Graphics/Interface.php +++ b/CRM/Mosaico/Graphics/Interface.php @@ -10,7 +10,7 @@ * probably be better. As long as it remains internal, we have some * flexibility to clean it up. */ -interface CRM_Mosaico_Graphics_Interface { +abstract class CRM_Mosaico_Graphics_Interface { /** * Generate a placeholder image. @@ -19,7 +19,7 @@ interface CRM_Mosaico_Graphics_Interface { * @param int $height * @return mixed */ - public function sendPlaceholder($width, $height); + abstract public function sendPlaceholder($width, $height); /** * Generate a scaled version of the image. @@ -40,7 +40,7 @@ public function sendPlaceholder($width, $height); * NOTE: NULL or 0 are interpreted "auto-scaled". * @return mixed */ - public function createResizedImage($src, $dest, $width, $height); + abstract public function createResizedImage($src, $dest, $width, $height); /** * Generate a "cover" version of the image. @@ -61,6 +61,56 @@ public function createResizedImage($src, $dest, $width, $height); * NOTE: NULL or 0 are interpreted "auto-scaled". * @return mixed */ - public function createCoveredImage($src, $dest, $width, $height); + abstract public function createCoveredImage($src, $dest, $width, $height); + + /** + * Adjust resize dimensions in order to preserve the best possible resolution for the image. + * + * @param int $imgWidth + * Image width in pixels. + * @param int $imgHeight + * Image height in pixels. + * @param int|NULL $resizeWidth + * Resize width in pixels. + * @param int|NULL $resizeHeight + * Resize height in pixels. + * @return float|null + */ + public function adjustResizeDimensions($imgWidth, $imgHeight, &$resizeWidth, &$resizeHeight) { + $scaleFactor = NULL; + $scales[Civi::settings()->get('mosaico_scale_width_limit1')] = Civi::settings()->get('mosaico_scale_factor1'); + $scales[Civi::settings()->get('mosaico_scale_width_limit2')] = Civi::settings()->get('mosaico_scale_factor2'); + $scales = array_filter($scales); + ksort($scales, SORT_NUMERIC); + if (!empty($scales) && $resizeWidth) { + foreach ($scales as $width => $slevel) { + if ($resizeWidth <= $width) { + $scaleFactor = $slevel; + break; + } + } + } + if (empty($scaleFactor)) { + return NULL; + } + // If scale-factor make new width bigger than that of image itself, re-compute scale-factor to + // maximum possible. + if ($scaleFactor && $resizeWidth && $imgWidth && ($imgWidth < ($resizeWidth * $scaleFactor))) { + $possibleLevels[] = $imgWidth / $resizeWidth; + } + if ($scaleFactor && $resizeHeight && $imgHeight && ($imgHeight < ($resizeHeight * $scaleFactor))) { + $possibleLevels[] = $imgHeight / $resizeHeight; + } + if (!empty($possibleLevels)) { + $scaleFactor = max($possibleLevels); + } + if ($scaleFactor && $resizeWidth) { + $resizeWidth = round($resizeWidth * $scaleFactor); + } + if ($scaleFactor && $resizeHeight) { + $resizeHeight = round($resizeHeight * $scaleFactor); + } + return $scaleFactor; + } } diff --git a/CRM/Mosaico/Graphics/Intervention.php b/CRM/Mosaico/Graphics/Intervention.php index e04749ec57..c75bf1aae2 100644 --- a/CRM/Mosaico/Graphics/Intervention.php +++ b/CRM/Mosaico/Graphics/Intervention.php @@ -9,7 +9,7 @@ * @see https://github.com/voidlabs/mosaico/blob/master/backend/README.txt * @see http://image.intervention.io/getting_started/introduction */ -class CRM_Mosaico_Graphics_Intervention implements CRM_Mosaico_Graphics_Interface { +class CRM_Mosaico_Graphics_Intervention extends CRM_Mosaico_Graphics_Interface { const FONT_PATH = 'packages/mosaico/dist/vendor/notoregular/NotoSans-Regular-webfont.ttf'; @@ -96,6 +96,7 @@ protected static function flattenPoints($points) { public function createResizedImage($srcFile, $destFile, $width, $height) { $config = CRM_Mosaico_Utils::getConfig(); $img = Image::make($srcFile); + $this->adjustResizeDimensions($img->width(), $img->height(), $width, $height); if ($width && $height) { $img->resize($width, $height); @@ -114,6 +115,7 @@ public function createResizedImage($srcFile, $destFile, $width, $height) { public function createCoveredImage($srcFile, $destFile, $width, $height) { $img = Image::make($srcFile); + $this->adjustResizeDimensions($img->width(), $img->height(), $width, $height); $ratios = []; if ($width) { diff --git a/CRM/Mosaico/Utils.php b/CRM/Mosaico/Utils.php index a449c326ed..6e89ad9ff8 100644 --- a/CRM/Mosaico/Utils.php +++ b/CRM/Mosaico/Utils.php @@ -51,6 +51,36 @@ public static function getGraphicsOptions() { ]; } + /** + * Get a list of image resize scale factors + * + * @return array + * Array (int $machineName => string $label). + */ + public static function getResizeScaleFactor() { + return [ + '' => E::ts('None'), + 3 => E::ts('3x'), + 2 => E::ts('2x'), + ]; + } + + /** + * Get a list of image resize scale width limits + * + * @return array + * Array (int $machineName => string $label). + */ + public static function getResizeScaleWidthLimit() { + return [ + '' => E::ts('None'), + 190 => E::ts('Upto 190 pixels (e.g 3 column blocks)'), + 285 => E::ts('Upto 285 pixels (e.g 2 column blocks)'), + 999 => E::ts('Upto 570 pixels (e.g 1 column blocks)'), + 9999 => E::ts('All (other) sizes'), + ]; + } + /** * Get the path to the Mosaico layout file. * @@ -178,7 +208,6 @@ public static function getConfig() { return $mConfig; } - /** * handler for upload requests */ diff --git a/docs/images/scaling-factor-config.png b/docs/images/scaling-factor-config.png new file mode 100644 index 0000000000..4996baffe1 Binary files /dev/null and b/docs/images/scaling-factor-config.png differ diff --git a/docs/images/scaling-factor-resolution-diff.png b/docs/images/scaling-factor-resolution-diff.png new file mode 100644 index 0000000000..9b8c6ea6bc Binary files /dev/null and b/docs/images/scaling-factor-resolution-diff.png differ diff --git a/docs/setup.md b/docs/setup.md new file mode 100644 index 0000000000..94675c88fc --- /dev/null +++ b/docs/setup.md @@ -0,0 +1,24 @@ +# Setup + +## Support for High Resolution Images + +Images when uploaded in 2 columns (e.g 258 x 100) or 3 (e.g 166 x 90) column blocks when upscaled to render on mobile devices, ends up with stretched out and lower resolution image. + +Introduced `Image resize scale factor` mosaico setting attempts to solve the problem by scaling uploaded images to 2x or 3x of their block size specially for 2 and 3 column layouts so upscale doesn't look distorted or low resolution. The solution also work for single column images. + +![](images/scaling-factor-config.png) + +Following example shows how a correctly configured scaling factor can improve the resolution of image rendered. + +![](images/scaling-factor-resolution-diff.png) + +__Scaling factor config example__: +3x => Upto 285 pixels (covers both 2 and 3 column block images) +2x => All other sizes (single column block images) + +Which means when an image is uploaded in a block with 285 pixels (or less), image gets trimmed to 285 * 3 pixels instead of 285 pixels. +For any image larger than that, size is reduced to 2x size of the block. + +__Note__: higher the scaling factor, higher the resolution but lower the compression. + +Scaling is not tied to any particular type of image. Png format supports lossless compression and therefore compression appears less than jpg images which support lossy compression. diff --git a/mkdocs.yml b/mkdocs.yml index 70af7b9274..f927730d68 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -17,6 +17,7 @@ markdown_extensions: nav: - About: index.md +- Setup: setup.md - API: api.md - Development: develop.md - Testing: testing.md diff --git a/settings/Mosaico.setting.php b/settings/Mosaico.setting.php index 3fab1e4acf..1c02218967 100644 --- a/settings/Mosaico.setting.php +++ b/settings/Mosaico.setting.php @@ -42,6 +42,90 @@ 'description' => NULL, 'help_text' => NULL, ], + 'mosaico_scale_factor1' => [ + 'group_name' => 'Mosaico Preferences', + 'group' => 'mosaico', + 'name' => 'mosaico_scale_factor1', + 'quick_form_type' => 'Select', + 'type' => 'String', + 'html_type' => 'select', + 'html_attributes' => [ + 'class' => 'crm-select2', + ], + 'pseudoconstant' => [ + 'callback' => 'CRM_Mosaico_Utils::getResizeScaleFactor', + ], + 'default' => '', + 'add' => '5.24', + 'title' => 'Image resize scale factor', + 'is_domain' => 1, + 'is_contact' => 0, + 'description' => NULL, + 'help_text' => NULL, + ], + 'mosaico_scale_factor2' => [ + 'group_name' => 'Mosaico Preferences', + 'group' => 'mosaico', + 'name' => 'mosaico_scale_factor2', + 'quick_form_type' => 'Select', + 'type' => 'String', + 'html_type' => 'select', + 'html_attributes' => [ + 'class' => 'crm-select2', + ], + 'pseudoconstant' => [ + 'callback' => 'CRM_Mosaico_Utils::getResizeScaleFactor', + ], + 'default' => '', + 'add' => '5.24', + 'title' => 'Image resize scale factor', + 'is_domain' => 1, + 'is_contact' => 0, + 'description' => NULL, + 'help_text' => NULL, + ], + 'mosaico_scale_width_limit1' => [ + 'group_name' => 'Mosaico Preferences', + 'group' => 'mosaico', + 'name' => 'mosaico_scale_width_limit1', + 'quick_form_type' => 'Select', + 'type' => 'String', + 'html_type' => 'select', + 'html_attributes' => [ + 'class' => 'crm-select2', + ], + 'pseudoconstant' => [ + 'callback' => 'CRM_Mosaico_Utils::getResizeScaleWidthLimit', + ], + 'default' => '', + 'add' => '5.24', + 'title' => 'Image resize scale factor width limit', + 'is_domain' => 1, + 'is_contact' => 0, + 'description' => NULL, + 'help_text' => NULL, + ], + 'mosaico_scale_width_limit2' => [ + 'group_name' => 'Mosaico Preferences', + 'group' => 'mosaico', + 'name' => 'mosaico_scale_width_limit2', + 'quick_form_type' => 'Select', + 'type' => 'String', + 'html_type' => 'select', + 'html_attributes' => [ + 'class' => 'crm-select2', + ], + 'pseudoconstant' => [ + 'callback' => 'CRM_Mosaico_Utils::getResizeScaleWidthLimit', + ], + 'default' => '', + 'add' => '5.24', + 'title' => 'Image resize scale factor width limit', + 'is_domain' => 1, + 'is_contact' => 0, + 'description' => NULL, + 'help_text' => NULL, + ], 'mosaico_custom_templates_dir' => [ 'group_name' => 'Mosaico Preferences', 'group' => 'mosaico', diff --git a/templates/CRM/Mosaico/Form/MosaicoAdmin.tpl b/templates/CRM/Mosaico/Form/MosaicoAdmin.tpl index 4a2230199c..ab894e1f85 100644 --- a/templates/CRM/Mosaico/Form/MosaicoAdmin.tpl +++ b/templates/CRM/Mosaico/Form/MosaicoAdmin.tpl @@ -34,6 +34,16 @@ {$form.mosaico_custom_templates_url.html|crmAddClass:'huge40'} + + + {$form.mosaico_scale_factor1.label} + + + {$form.mosaico_scale_factor1.html|crmAddClass:six} {ts}for resize of images with width{/ts} {$form.mosaico_scale_width_limit1.html|crmAddClass:huge}
+ {$form.mosaico_scale_factor2.html|crmAddClass:six} {ts}for resize of images with width{/ts} {$form.mosaico_scale_width_limit2.html|crmAddClass:huge}
+ {ts}When uploading images, the mosaico editor trims it down to very required size (in pixels). Use scale factor setting to keep some buffer (2x or 3x) so upscale doesn't look distorted or low resolution. Example:{/ts}
{ts}3x => Upto 285 pixels (covers both 2 and 3 column block images){/ts}
{ts}2x => All other sizes (single column block images){/ts}
+ +
{include file="CRM/common/formButtons.tpl" location="bottom"}
diff --git a/tests/phpunit/CRM/Mosaico/ResizeScaleTest.php b/tests/phpunit/CRM/Mosaico/ResizeScaleTest.php new file mode 100644 index 0000000000..fb4d04a30b --- /dev/null +++ b/tests/phpunit/CRM/Mosaico/ResizeScaleTest.php @@ -0,0 +1,79 @@ +set('mosaico_scale_width_limit1', 190); + Civi::settings()->set('mosaico_scale_factor1', 3); + // 2x - not set + Civi::settings()->set('mosaico_scale_width_limit2', ''); + Civi::settings()->set('mosaico_scale_factor2', ''); + + $graphics = CRM_Mosaico_Services::createGraphics(); + + // test resize for 166x90 - matches settings, for an image of size 1080x1080 + $resizeWidth = 166; + $resizeHeight = 90; + $graphics->adjustResizeDimensions(1080, 1080, $resizeWidth, $resizeHeight); + // dimensions should be 3x now + $this->assertEquals(166 * 3, $resizeWidth); + $this->assertEquals(90 * 3, $resizeHeight); + + // test resize for 258x100 - does not match settings + $resizeWidth = 258; + $resizeHeight = 100; + $graphics->adjustResizeDimensions(1080, 1080, $resizeWidth, $resizeHeight); + // dimensions should remain same + $this->assertEquals(258, $resizeWidth); + $this->assertEquals(100, $resizeHeight); + + // 2x - add settings + Civi::settings()->set('mosaico_scale_width_limit2', '285'); + Civi::settings()->set('mosaico_scale_factor2', '2'); + + // test resize for 258x100 - matches settings + $resizeWidth = 258; + $resizeHeight = 100; + $graphics->adjustResizeDimensions(1080, 1080, $resizeWidth, $resizeHeight); + // dimensions should be 2x now + $this->assertEquals(258 * 2, $resizeWidth); + $this->assertEquals(100 * 2, $resizeHeight); + + // test over scaling i.e scaling would result in more size than the image + $resizeWidth = 258; + $resizeHeight = 100; + $sf = $graphics->adjustResizeDimensions(358, 200, $resizeWidth, $resizeHeight); + // dimensions should be almost same as that of image ignoring the scaling config, + // keeping the ratio of resize dimensions + $this->assertEquals(round($sf * 258), $resizeWidth); + $this->assertEquals(round($sf * 100), $resizeHeight); + + // test no scaling - configs not set + Civi::settings()->set('mosaico_scale_width_limit1', ''); + Civi::settings()->set('mosaico_scale_factor1', ''); + Civi::settings()->set('mosaico_scale_width_limit2', ''); + Civi::settings()->set('mosaico_scale_factor2', ''); + $resizeWidth = 258; + $resizeHeight = 100; + $graphics->adjustResizeDimensions(1080, 1080, $resizeWidth, $resizeHeight); + // dimensions should remain same + $this->assertEquals(258, $resizeWidth); + $this->assertEquals(100, $resizeHeight); + } + +}