9

I have an array:

$resolutions = array(
    '480x640',
    '480x800',
    '640x480',
    '640x960',
    '800x1280',
    '2048x1536'
);

I want to retrieve closest larger value with the nearest aspect ratio (same orientation).

So, in case of $needle = '768x1280' - 800x1280.
And, in case of $needle = '320x240' - 640x480. While the closest here is 480x640 it shouldn't be matched, because its aspect ratio differs too much. So on, and so forth.

Purpose:

I have a set of images with resolutions as specified in $resolutions. Those images are going to be used for smartphone wallpapers.

With JavaScript, I am sending over a request with screen.width and screen.height to determine $needle.

On the server side, I am going to fetch the closest larger value of the given resolution, scale it down to fit the whole screen while preserving aspect ratio, and if something overlaps the dimensions, crop it to perfectly fit the screen.

Problem:

While everything is pretty simple with scaling and cropping, I cannot think of a way to find out the closest larger value, to load the reference image.

Hints:

In case it helps, $resolutions and $needle can be in a different format, ie.: array('width' => x, 'height' => y).

Tries:

I tried to experiment with levenshtein distance: http://codepad.viper-7.com/e8JGOw
Apparently, it worked only for 768x1280 and resulted 800x1280. For 320x240 it resulted in 480x640 but that does not fit this time.

Community
  • 1
  • 1
tomsseisums
  • 13,168
  • 19
  • 83
  • 145
  • 1
    In your second example `$needle = '320x240' - 640x480.` should'nt select instead `480x640` ?, as 480 is closer than 640.. or are you finding first closer on the B part of AxB .. please clarify what you want.. – Nelson Dec 10 '12 at 17:01
  • first step will be splitting those strings into separate x & y resolutions. otherwise you'll be doing a lot of repetitive string operations. – Marc B Dec 10 '12 at 17:02
  • is orientation not important? – Popnoodles Dec 10 '12 at 17:06
  • @Nelson, misleading, yes - I've edited to add the `with nearest aspect ratio` part. @popnoodles, it is - edited. – tomsseisums Dec 10 '12 at 17:06
  • @Ricardo, as of writing - nothing. Currently am experimenting with "Levenshtein distance", before moving forwards. – tomsseisums Dec 10 '12 at 17:11

8 Answers8

10

Try this

echo getClosestRes('500x960');
echo '<br /> try too large to match: '.getClosestRes('50000x960');

function getClosestRes($res){
    $screens = array(
        'landscape'=>array(
            '640x480',
            '1200x800'
        ),
        'portrait'=>array(
            '480x640',
            '480x800',
            '640x960',
            '800x1280',
            '1536x2048'
        )
    );

    list($x,$y)=explode('x',$res);
    $use=($x>$y?'landscape':'portrait');

    // if exact match exists return original
    if (array_search($res, $screens[$use])) return $res; 

    foreach ($screens[$use] as $screen){
        $s=explode('x',$screen);
        if ($s[0]>=$x && $s[1]>=$y) return $screen;
    }
    // just return largest if it gets this far.
    return $screen; // last one set to $screen is largest
}
Popnoodles
  • 28,090
  • 2
  • 45
  • 53
1

Made a quick class. Should competently find the minimum resolution for any two numbers that you specify. I have preloaded it with the resolutions you specified but the $_resolutions array could be set to whichever standards you like, and can also be changed on-the-fly.

class Resolution {

    /**
     * Standard resolutions
     *
     * Ordered by smallest to largest width, followed by height.
     *
     * @var array
     */
    private $_resolutions = array(
        array('480', '640'),
        array('480', '800'),
        array('640', '480'),
        array('640', '960'),
        array('800', '1280'),
        array('2048', '1536')
    );

    /**
     * Width
     *
     * @var int
     */
    private $_width;

    /**
     * Height
     *
     * @var int
     */
    private $_height;

    /**
     * Constructor
     *
     * @param  int $width
     * @param  int $height
     * @return void
     */
    public function __construct($width, $height) {
        $this->setSize($width, $height);
    }

    /**
     * Find the minimum matched standard resolution
     *
     * @param  bool $revertToLargest (OPTIONAL) If no large enough resolution is found, use the largest available.
     * @param  bool $matchAspectRatio (OPTIONAL) Attempt to get the closest resolution with the same aspect ratio. If no resolutions have the same aspect ratio, it will simply use the minimum available size.
     * @return array The matched resolution width/height as an array.  If no large enough resolution is found, FALSE is returned, unless $revertToLargest is set.
     */
    public function getMinimumMatch($revertToLargest = false, $matchAspectRatio = true) {
        if ($matchAspectRatio) {
            $aspect = $this->_width/$this->_height;
            foreach ($this->_resolutions as $res) {
                if ($res[0]/$res[1] == $aspect) {
                    if ($this->_width > $res[0] || $this->_height >     $res[1]) {
                        return ($revertToLargest ? $res : false);
                    }
                    return $res;
                }
            }
        }
        foreach ($this->_resolutions as $i => $res) {
            if ($this->_width <= $res[0]) {
                $total = count($this->_resolutions);
                for ($j = $i; $j < $total; $j++) {
                    if ($this->_height <= $this->_resolutions[$j][1]) {
                        return $this->_resolutions[$j];
                    }
                }
            }
        }
        return ($revertToLargest ? end($this->_resolutions) : false);
    }

    /**
     * Get the resolution
     *
     * @return array The resolution width/height as an array
     */
    public function getSize() {
        return array($this->_width, $this->_height);
    }

    /**
     * Set the resolution
     *
     * @param  int $width
     * @param  int $height
     * @return array The new resolution width/height as an array
     */
    public function setSize($width, $height) {
        $this->_width = abs(intval($width));
        $this->_height = abs(intval($height));
        return $this->getSize();
    }

    /**
     * Get the standard resolutions
     *
     * @return array
     */
    public function getStandardResolutions() {
        return $this->_resolutions;
    }

    /**
     * Set the standard resolution values
     *
     * @param  array An array of resolution width/heights as sub-arrays
     * @return array
     */
    public function setStandardResolutions(array $resolutions) {
        $this->_resolutions = $resolutions;
        return $this->_resolutions;
    }

}

Example Usage

$screen = new Resolution(320, 240);
$screen->getMinimumMatch();
// Returns 640 x 480 (aspect ratio matched)

$screen = new Resolution(1280, 960);
$screen->getMinimumMatch();
// Returns 640 x 480 (aspect ratio matched)

$screen = new Resolution(400, 960);
$screen->getMinimumMatch();
// Returns 640 x 960 (aspect ratio not matched, so uses closest fit)

$screen = new Resolution(5000, 5000);
$screen->getMinimumMatch();
// Returns FALSE (aspect ratio not matched and resolution too large)

$screen = new Resolution(5000, 5000);
$screen->getMinimumMatch(true);
// Returns 2048 x 1536 (aspect ratio not matched and resolution too large, so uses largest available)
BadHorsie
  • 14,135
  • 30
  • 117
  • 191
  • The code is pretty clean, great effort, just that the `320x240` returns `480x640`. While it is the closest, the aspect ratio doesn't fit, so the closest in this case should be 640x480 (exact double). +1 for the effort and cleanliness, though. – tomsseisums Dec 11 '12 at 06:54
  • @psycketom Ah, I didn't realise you wanted to keep the aspect ratio as well. Try the updated code. – BadHorsie Dec 14 '12 at 11:52
0

Would it be easier if you had a single number to compare against?

It's a ratio, so just do, for example: 640 / 480 = 1.33*

Then you at least have something nice and simple to compare against the dimensions you are sending and presumably come up with a tolerance?

A simple example, which assume that the ratio array is ordered from lowest to highest. If this was a problem then we would create a search that ordered by the area (x by y).

function getNearestRatio($myx, $myy)
{

    $ratios = array(
        array('x'=>480, 'y'=>640),
        array('x'=>480, 'y'=>800),
        array('x'=>640, 'y'=>480),
        array('x'=>640, 'y'=>960),
        array('x'=>800, 'y'=>1280),
        array('x'=>2048, 'y'=>1536)
    );
    $tolerance = 0.1;
    foreach ($ratios as $ratio) {
         $aspect = $ratio['x'] / $ratio['y'];
        $myaspect = $myx / $myy;

        if ( ! ($aspect - $tolerance < $myaspect && $myaspect < $aspect + $tolerance )) {
            continue;
        }

        if ($ratio['x'] < $myx || $ratio['y'] < $myy) {
            continue;
        }
        break;
    }

    return $ratio;
}

I've built in a tolerance, so that it will match 'nearby' aspect ratios, as you allude to in your question.

This function should pass both test cases you have given.

Paul S
  • 1,229
  • 9
  • 17
  • that would find closest ratio not closest larger (or equal) size – Popnoodles Dec 10 '12 at 17:25
  • Surely it is trivial to then find the closest value? I was assuming the difficulty was in finding the closest aspect ratio. – Paul S Dec 10 '12 at 17:33
  • 1
    go on then (without looking at the answers given) – Popnoodles Dec 10 '12 at 17:37
  • again, that would find closest ratio not closest larger (or equal) size – Popnoodles Dec 11 '12 at 17:21
  • how so? have you tested this with inputs and expected outputs? – Paul S Dec 11 '12 at 17:23
  • "function getNearestRatio()" – Popnoodles Dec 11 '12 at 17:24
  • As opposed to "getClosestRes()"?... the function works to the best of my knowledge. If you can find any problems other than the example name I gave it, then I can take a look at correcting them. If not, you're just being childish. – Paul S Dec 11 '12 at 17:30
  • You've put "A simple example, which assume that the ratio array is ordered from lowest to highest.", but the ratio isn't - it will vary totally independent of increase in screen size and sometimes it may be the same as a previous ratio for another resolution. – Popnoodles Dec 11 '12 at 17:43
0

You can first extract the arrays like:

$resolutions = array(
    '480x640',
    '480x800',
    '640x480',
    '640x960',
    '800x1280',
    '2048x1536'
);

foreach ($resolutions as $resolution):
    $width[]=(int)$resolution;
    $height[]=(int)substr(strrchr($resolution, 'x'), 1);
    echo $width,' x ',$height,'<br>';
endforeach;

Then you can match the given needle with the array with in_array and array_search like:

$key = array_search('480', $items);
echo $key;  

When you have the key just increment it for the closest greater value. I'll let you do that by yourself.

Lenin
  • 570
  • 16
  • 36
0

Okay, I have it. I've written a function that returns the lowest suitable resolution, and accounts for nonstandard resolutions as well.

    <?php
    //some obscure resolution, for illustrative purposes
    $theirResolution = '530x700'; 
    $resolutions = array(
        '480x640',
        '480x800',
        '640x480',
        '640x960',
        '800x1280',
        '2048x1536'
    );

    function findSmallestResolution($theirResolution,$resolutions){
        $temp = explode('x',$theirResolution);
        //Isolate their display's X dimension
        $theirResolutionX = intval($temp[1]);
        foreach($resolutions as $key => $value){
            $temp = explode('x',$value);
            //if the current resolution is bigger than or equal to theirs in the X dimension, then it's a possibility.
            if($theirResolutionX <= intval($temp[1])){
                $possibleResolutionsX[] = $value;
            }
        }
        //Now we'll filter our $possibleResolutions in the Y dimension.
        $temp = explode('x',$theirResolution);
        //Isolate their display's Y dimension
        $theirResolutionY = intval($temp[0]);
        foreach($possibleResolutionsX as $key => $value){
            $temp = explode('x',$value);
            //if the current resolution is bigger than or equal to theirs in the X dimension, then it's a possibility.
            if($theirResolutionY <= intval($temp[0])){
                $possibleResolutionsXY[] = $value;
            }
        }
        //at this point, $possibleResolutionsXY has all of our entries that are big enough. Now to find the smallest among them.
        foreach($possibleResolutionsXY as $key => $value){
            $temp = explode('x', $value);
            //since we didn't specify how standard our app's possible resolutions are, I'll have to measure the smallest in terms of total dots and not simply X and Y.
            $dotCount[] = intval($temp[0]) * intval($temp[1]);
        }
        //find our resolution with the least dots from the ones that still fit the user's.
        foreach($dotCount as $key => $value){
            if($value == min($dotCount)){
                $minkey = $key;
            }
        }
        //use the key from dotCount to find its corresponding resolution from possibleResolutionsXY.
        return $possibleResolutionsXY[$minkey];
    }


    findSmallestResolution($theirResolution,$resolutions);
    // returns '640x960'.


    ?>
  • It failed on `240x320`. Where it should've returned `480x640`, it returned `640x480`. – tomsseisums Dec 10 '12 at 17:30
  • By the conditions that the asker has defined, this is not a failure. It is one of the set of smallest resolutions that can encapsulate the user's. Nonetheless, you're correct in that this solution does seem counter-intuitive. – Vail DeGraff Dec 10 '12 at 18:03
0

First of all, I would store the haystack using width first, height second:

$resolutions = array(
    array('w' => 640, 'h' => 480),
    array('w' => 800, 'h' => 480),
    array('w' => 960, 'h' => 640),
    array('w' => 1280, 'h' => 800),
    array('w' => 2048, 'h' => 1536),
);

Then, calculate dimension differences between needle and each item, followed by the area size:

array_walk($resolutions, function(&$item) use ($needle) {
    $item['aspect'] = abs($item['w'] - $needle['w']) / abs($item['h'] - $needle['h']);
    $item['area'] = $item['w'] * item['h'];
});

usort($resolutions, function($a, $b) {
  if ($a['aspect'] != $b['aspect']) {
    return ($a['aspect'] < $b['aspect']) ? -1 : 1;
  }
 return 0;
});

Then you filter the list based on which resolutions are bigger; the first match is the one closest to the needle aspect ratio:

$needle_area = $needle['w'] * $needle['h'];
foreach ($resolutions as $item) {
    if ($needle_area < $item['area']) {
        return $item;
    }
}
return null;
Lenin
  • 570
  • 16
  • 36
Ja͢ck
  • 170,779
  • 38
  • 263
  • 309
0

Well, this turned out larger than I anticipated, but I think this meets the criteria. It works by breaking the available resolutions down to their ratio. Then sorting by the delta between the target ratio and the available ratios ascending, then by size (pixels) descending. Returning the top match - which should be the closest, smallest match.

class ResolutionMatcher
{
    private $resolutions;

    public function __construct(array $resolutions)
    {
        foreach ($resolutions as $resolution) {
            $this->resolutions[$resolution] = $this->examineResolution($resolution);
        }
    }

    public function findClosest($target)
    {
        $targetDetails = $this->examineResolution($target);
        $deltas = array();
        foreach ($this->resolutions as $resolution => $details) {
            if ($details['long'] < $targetDetails['long'] || $details['short'] < $targetDetails['short']) continue;
            $deltas[$resolution] = array(
                'resolution' => $resolution,
                'delta' => abs($details['ratio'] - $targetDetails['ratio']),
            );
        }
        $resolutions = $this->resolutions;
        uasort($deltas, function ($a, $b) use ($resolutions) {
            $deltaA = $a['delta'];
            $deltaB = $b['delta'];
            if ($deltaA === $deltaB) {
                $pixelsA = $resolutions[$a['resolution']]['pixels'];
                $pixelsB = $resolutions[$b['resolution']]['pixels'];
                if ($pixelsA === $pixelsB) {
                    return 0;
                }
                return $pixelsA > $pixelsB ? 1 : -1;
            }
            return $deltaA > $deltaB ? 1 : -1;
        });
        $resolutions = array_keys($deltas);
        return array_pop($resolutions);
    }

    private function examineResolution($resolution)
    {
        list($width, $height) = explode('x', $resolution);
        $long = ($width > $height) ? $width : $height;
        $short = ($width < $height) ? $width : $height;
        $ratio = $long / $short;
        $pixels = $long * $short;
        return array(
            'resolutions' => $resolution,
            'pixels' => $pixels,
            'long' => $long,
            'short' => $short,
            'ratio' => $ratio,
        );
    }
}

Usage:

$resolutions = array(
    '480x640',
    '480x800',
    '640x480',
    '640x960',
    '800x1280',
    '2048x1536'
);

$target = $_GET['target'];

$matcher = new ResolutionMatcher($resolutions);
$closest = $matcher->findClosest($target);
Brenton Alker
  • 8,947
  • 3
  • 36
  • 37
0

if you are just looking for the nearest ratio, try this:

echo getClosestRatio(1.20); //gets 1.19
echo getClosestRatio(1.09); //gets 1
echo getClosestRatio(1.30); //gets 1.3333333

function getClosestRatio($fx){
    $ratio = array(
            1,
            1.19,
            1.25,
            (4/3),
            1.3375,
            1.43,
            1.5,
            1.56,
            1.6180,
            5/3,
            16/9,
            1.85,
            1.9,
            2/1
      
        );

  $min=[];
    foreach ($ratio as $screen){
      # if($fx==$screen){return $screen;}
        $diff=abs($screen-$fx);
        $min["".$diff]=$screen;
    }
    ksort($min);

    return array_values($min)[0];
}
dazzafact
  • 2,570
  • 3
  • 30
  • 49