0

I am trying to get the aspect ratio of a photo but the following code shows wrong aspect ratio on a photo with 3776 in width and 2520 in height (472:315) but it shows correct aspect ratio on a photo with 3968 in width and 2232 in height though (16:9).

function gcd($a, $b) {
    if($a == 0 || $b == 0) {
        return abs(max(abs($a), abs($b)));
    }

    $r = $a % $b;
    return ($r != 0) ? gcd($b, $r) : abs($b);
}

$gcd = gcd($widtho, $heighto);
echo ($widtho / $gcd).':'.($heighto / $gcd);

How can I solve my problem?

Thanks in advance.

Airikr
  • 6,258
  • 15
  • 59
  • 110
  • Why is this not working? You've found the aspect ratio according to your code (which works). Where is the problem? – Reut Sharabani Oct 19 '12 at 03:11
  • The photo with 3776 in width and 2520 in height is taken with an aspect ratio of 3:2. Not 472:315 (does that aspect ratio even exists?!). Therefore my problem. – Airikr Oct 19 '12 at 03:11
  • Actually, 3780x2520 is an aspect ratio of 3:2; because you're using 3776 for the width, 472:315 is the correct ratio. If you do the division, it comes out to 1.498, which is pretty close-enough. If you want "perfect" ratios (like standard "3:2" or "16:9"), you could pass the detected ratio to another function that rounds them to find the nearest/best-match instead. – newfurniturey Oct 19 '12 at 03:19
  • Hm. Okey. How should that function looks like? I'm a beginner in calculating sizes in PHP. – Airikr Oct 19 '12 at 03:22

2 Answers2

3

Actually, 3780x2520 is an aspect ratio of 3:2; because you're using 3776 for the width, 472:315 is the correct ratio. If you do the division, it comes out to 1.498, which is pretty close-enough to 1.5 to consider rounding to 3:2.

If you want only "standard" ratios (like "3:2" or "16:9"), you could pass the detected ratio to another function that rounds them to find the nearest/best-match instead.

This is a thrown-together function that can do the rounding for you (only tested against the dimensions in your example, so I can't guarantee 100% success yet):

function findBestMatch($ratio) {
    $commonRatios = array(
        array(1, '1:1'), array((4 / 3), '4:3'), array((3 / 2), '3:2'),
        array((5 / 3), '5:3'), array((16 / 9), '16:9'), array(3, '3')
    );

    list($numerator, $denominator) = explode(':', $ratio);
    $value = $numerator / $denominator;

    $end = (count($commonRatios) - 1);
    for ($i = 0; $i < $end; $i++) {
        if ($value == $commonRatios[$i][0]) {
            // we have an equal-ratio; no need to check anything else!
            return $commonRatios[$i][1];
        } else if ($value < $commonRatios[$i][0]) {
            // this can only happen if the ratio is `< 1`
            return $commonRatios[$i][1];
        } else if (($value > $commonRatios[$i][0]) && ($value < $commonRatios[$i + 1][0])) {
            // the ratio is in-between the current common-ratio and the next in the list
            // find whichever one it's closer-to and return that one.
            return (($value - $commonRatios[$i][0]) < ($commonRatios[$i + 1][0] - $value)) ? $commonRatios[$i][1] : $commonRatios[$i + 1][1];
        }
    }

    // we didn't find a match; that means we have a ratio higher than our biggest common one
    // return the original value
    return $ratio;
}

To use this function, you pass in the ratio-string (not the numeric value) to it and it will attempt to "find a best match" in the common list of ratios.

Example usage:

$widtho = 3968;
$heighto = 2232;
$gcd = gcd($widtho, $heighto);
$ratio = ($widtho / $gcd).':'.($heighto / $gcd);
echo 'found: ' . $ratio . "\n";
echo 'match: ' . findBestMatch($ratio) . "\n";

$widtho = 3776;
$heighto = 2520;
$gcd = gcd($widtho, $heighto);
$ratio = ($widtho / $gcd).':'.($heighto / $gcd);
echo 'found: ' . $ratio . "\n";
echo 'match: ' . findBestMatch($ratio) . "\n";

$widtho = 3780;
$heighto = 2520;
$gcd = gcd($widtho, $heighto);
$ratio = ($widtho / $gcd).':'.($heighto / $gcd);
echo 'found: ' . $ratio . "\n";
echo 'match: ' . findBestMatch($ratio) . "\n";

The above test will output the following:

found: 16:9
match: 16:9

found: 472:315
match: 3:2

found: 3:2
match: 3:2

* I took the list of "standard" aspect ratios from wikipedia, if you want a reference.

newfurniturey
  • 37,556
  • 9
  • 94
  • 102
  • 2
    @ErikEdgren Awesome, glad I could help. If you notice any issues in the future let me know and I can revise - who knows who else will need this one too =P – newfurniturey Oct 19 '12 at 03:53
  • Very true :) I'll come back if I need more help with this function. – Airikr Oct 19 '12 at 03:55
  • 1000:1333 should be 3:4, so this will not handle such case. – funguy Mar 14 '17 at 14:54
  • @MrUpsidown Sure, but you have to realize that a StackOverflow "answer" is more of a guide to how to accomplish something and not a "here's the perfect solution on a silver platter" =P. Also, my answer (from 8 years ago) is regarding "standard" aspect ratios - none of which have a height bigger than a width. I'm glad you provided a separate answer to help others with that case too though, hopefully it helps everyone out =] – newfurniturey Mar 18 '20 at 19:29
  • I sure do, I was just pointing out that issue for anyone interested in your solution. – MrUpsidown Mar 18 '20 at 19:41
  • Is there really no simpler solution to this is PHP? all I was is the x:y aspect ratio. Why does this need to be so complicated? – Tim Bogdanov Mar 30 '22 at 22:58
  • @TimBogdanov The solution provided here is for multiple different aspect ratios and finding the best match. If you know the exact ratio you're after, the code can be more compact and cleaner for sure. – newfurniturey Apr 14 '22 at 12:08
2

I found this gist which works nicely in Javascript. So I ported it to PHP.

  /**
   * Returns the nearest aspect ratio of a width and height within a limited range of possible aspect ratios.
   * In other words, while 649x360 technically has an aspect ratio of 649:360,
   * it’s often useful to know that the nearest normal aspect ratio is actually 9:5 (648x360).
   * @param $width
   * @param $height
   * @param int $side The nearest ratio to side with. A number higher than zero
   * tells the function to always return the nearest ratio that is equal or higher
   * than the actual ratio, whereas a smaller number returns the nearest ratio higher
   * that is equal or smaller than the actual ratio. Defaults to 0.
   * @param int $maxW The maximum width in the nearest normal aspect ratio. Defaults to 16.
   * @param int $maxH The maximum height in the nearest normal aspect ratio. Defaults to 16.
   * @return string|null
   */

   function closestAspectRatio($width, $height, $side = 0, $maxW = 16, $maxH = 16) {

    $ratiosT = [];
    $ratios = [];
    $ratio = ($width * 100) / ($height * 100);
    $maxW++;
    $maxH++;

    for ($w = 1; $w < $maxW; $w++) {
      for ($h = 1; $h < $maxH; $h++) {
        $ratioX = strval(($w * 100) / ($h * 100));
        if (!array_key_exists($ratioX, $ratiosT)) {
          $ratiosT[$ratioX] = TRUE;
          $ratios[$w . ':' . $h] = $ratioX;
        }
      }
    }

    $match = NULL;

    foreach ($ratios as $key => $value) {

      if (!$match || (!$side && abs($ratio - $value) < abs($ratio - $ratios[$match])
        ) || (
          $side < 0 && $value <= $ratio && abs($ratio - $value) < abs($ratio - $ratios[$match])
        ) || (
          $side > 0 && $value >= $ratio && abs($ratio - $value) < abs($ratio - $ratios[$match])
        )) {
        $match = $key;
      }
    }

    return $match;
  }

Use it this way (with all parameters):

print closestAspectRatio(3776, 2520, 0, 16, 16); // prints 3:2
print closestAspectRatio(2520, 3776, 0, 16, 16); // prints 2:3

Here is the JS demo for reference:

console.log('3776 x 2520 = ' + nearestNormalAspectRatio(3776, 2520, 0));
console.log('2520 x 3776 = ' + nearestNormalAspectRatio(2520, 3776, 0));

function nearestNormalAspectRatio(width, height, side) {
  var
    ratio = (width * 100) / (height * 100),
    maxW = 3 in arguments ? arguments[2] : 16,
    maxH = 4 in arguments ? arguments[3] : 16,
    ratiosW = new Array(maxW).join(',').split(','),
    ratiosH = new Array(maxH).join(',').split(','),
    ratiosT = {},
    ratios = {},
    match,
    key;

  ratiosW.forEach(function(empty, ratioW) {
    ++ratioW;

    ratiosH.forEach(function(empty, ratioH) {
      ++ratioH;

      ratioX = (ratioW * 100) / (ratioH * 100);

      if (!ratiosT[ratioX]) {
        ratiosT[ratioX] = true;

        ratios[ratioW + ':' + ratioH] = ratioX;
      }
    });
  });

  for (key in ratios) {
    if (!match || (!side && Math.abs(ratio - ratios[key]) < Math.abs(ratio - ratios[match])) || (
        side < 0 && ratios[key] <= ratio && Math.abs(ratio - ratios[key]) < Math.abs(ratio - ratios[match])
      ) || (
        side > 0 && ratios[key] >= ratio && Math.abs(ratio - ratios[key]) < Math.abs(ratio - ratios[match])
      )) {
      match = key;
    }
  }

  return match;
}
MrUpsidown
  • 21,592
  • 15
  • 77
  • 131