filepond icon indicating copy to clipboard operation
filepond copied to clipboard

[Feature] Aspect ratio validation

Open chrisbeach opened this issue 1 year ago • 4 comments

Is there an existing issue for this?

  • [X] I have searched the existing issues

Is your feature request related to a problem? Please describe.

My users may upload a photo that meets the resolution and size requirements but whose aspect ratio (width:height ratio) makes it very tall or very wide. I'd prefer to restrict uploads to photos that will be closer to square (but not necessarily square), for consistent appearance when viewing photos.

Describe the solution you'd like

I'd like to be able to use a plugin that allows me to specify a maximum aspect ratio.

If I specified a maximum of 1.0 then images must be square. If I specified a maximum of 2.0 then images may be rectangular, but if the height is more than double the width, or the width is more than double the height, then the image is rejected with a message: "too wide" or "too tall"

Describe alternatives you've considered

I tried the image-validate-size plugin but this didn't offer aspect ratio validation.

chrisbeach avatar Aug 17 '22 15:08 chrisbeach

I advise to fork the validate size plugin and adjust it to test a min and max aspect ratio. you get aspect ratio by dividing image width by image height.

rikschennink avatar Aug 17 '22 18:08 rikschennink

Will do thanks @rikschennink

chrisbeach avatar Aug 17 '22 18:08 chrisbeach

Here's a quick reference. Checks if a selected image is a square:

/* eslint-disable */

(function(global, factory) {
  typeof exports === 'object' && typeof module !== 'undefined'
    ? (module.exports = factory())
    : typeof define === 'function' && define.amd
    ? define(factory)
    : ((global = global || self),
      (global.FilePondPluginSquareCheck = factory()));
})(this, function() {
  'use strict';

  var plugin = function plugin(_ref) {
    var addFilter = _ref.addFilter,
      utils = _ref.utils;
    // get quick reference to Type utils
    var Type = utils.Type,
      isString = utils.isString,
      replaceInString = utils.replaceInString,
      guesstimateMimeType = utils.guesstimateMimeType,
      getExtensionFromFilename = utils.getExtensionFromFilename,
      getFilenameFromURL = utils.getFilenameFromURL;

    var validateFile = function validateFile(
      item
    ) {

      // use type detector
      return new Promise(function(resolve, reject) {
        let URL = (window.URL || window.webkitURL)
        const blob = URL.createObjectURL(item)
        let img = new Image()
        img.onload = () => {
          if (img.height !== img.width) {
            reject()
          }
          resolve()
        }
        img.src = blob
      });
    };

    var applyMimeTypeMap = function applyMimeTypeMap(map) {
      return function(acceptedFileType) {
        return map[acceptedFileType] === null
          ? false
          : map[acceptedFileType] || acceptedFileType;
      };
    };

    // filtering if an item is allowed in hopper
    addFilter('ALLOW_HOPPER_ITEM', function(file, _ref2) {
      var query = _ref2.query;
      // if we are not doing file type validation exit
      if (!query('GET_ALLOW_SQUARE_VALIDATION')) {
        return true;
      }

      return validateFile(file);
    });

    // called for each file that is loaded
    // right before it is set to the item state
    // should return a promise
    addFilter('LOAD_FILE', function(file, _ref3) {
      var query = _ref3.query;
      return new Promise(function(resolve, reject) {
        if (!query('GET_ALLOW_SQUARE_VALIDATION')) {
          resolve(file);
          return;
        }

        // if invalid, exit here
        var validationResult = validateFile(
          file,
        );

        var handleRejection = function handleRejection() {
          reject({
            status: {
              main: query('GET_LABEL_INVALID_ASPECT_RATIO'),
              sub: query('GET_LABEL_INVALID_ASPECT_RATIO')
            }
          });
        };

        // has returned new filename immidiately
        if (typeof validationResult === 'boolean') {
          if (!validationResult) {
            return handleRejection();
          }
          return resolve(file);
        }

        // is promise
        validationResult
          .then(function() {
            resolve(file);
          })
          .catch(handleRejection);
      });
    });

    // expose plugin
    return {
      // default options
      options: {
        // Enable or disable file type validation
        allowSquareValidation: [false, Type.BOOLEAN],

        // label to show when a type is not allowed
        labelInvalidAspectRatio: ['Image is not a square', Type.STRING],
      }
    };
  };

  // fire pluginloaded event if running in browser, this allows registering the plugin when using async script tags
  var isBrowser =
    typeof window !== 'undefined' && typeof window.document !== 'undefined';
  if (isBrowser) {
    document.dispatchEvent(
      new CustomEvent('FilePond:pluginloaded', { detail: plugin })
    );
  }

  return plugin;
});

michaelbukachi avatar Jun 22 '23 23:06 michaelbukachi

So, I racked my brain over a similar question for a long time. It may help you out here.

1 is square. Less than 1 is portrait. Greater than 1 is landscape.

If the image is too tall or too wide consider adding the image editor and present the user with the option to crop it to fit the desired ratio.

You could even add guides that show the preferred and closest aspect ratio in the editor.

marcusawereally avatar Jan 18 '24 06:01 marcusawereally