4
\$\begingroup\$

I got a function to recalculate the image size based on two settings.

  • Force ratio, force the requested ratio, default the original ratio will be used.
  • Keepsmaller, the uploaded image may never become bigger

If keep smaller is not enabled, by default we will focus calculation on the first reached, it's hard to explain see enclosed excel screen you will probebly understand.

<?php
function recalc($oldWidth, $oldHeight, $reqWidth, $reqHeight, $forceRatio = false, $keepSmaller = false){
    if ($keepSmaller) {
        $maxWidth   = ($oldWidth < $reqWidth)   ? $oldWidth  : $reqWidth;
        $maxHeight  = ($oldHeight < $reqHeight)  ? $oldHeight : $reqHeight;
    } else {
        $maxWidth   = $reqWidth;
        $maxHeight  = $reqHeight;
    }

    $ratio      = ($forceRatio) ? ($reqWidth / $reqHeight) : ($oldWidth / $oldHeight);

    if ($ratio != ($maxWidth / $maxHeight))
    {
        if ($keepSmaller && $ratio > ($maxWidth / $maxHeight)){
            $maxHeight = $maxWidth / $ratio;
        } elseif (!$forceRatio && $ratio < ($maxWidth / $maxHeight)) {
            $maxWidth  = $maxHeight * $ratio;
        }
    }

    echo $maxWidth . ', ' . $maxHeight;
}

recalc( 480,  360, 1440, 1080); echo "<br />";
recalc(1920, 1440, 1440, 1080); echo "<br />";
recalc( 480,  360, 1920, 1080); echo "<br />";
recalc(3840, 2880, 1920, 1080); echo "<br />";
echo "<br/>";

recalc( 480,  360, 1440, 1080, false, true); echo "<br />";
recalc(1920, 1440, 1440, 1080, false, true); echo "<br />";
recalc( 480,  360, 1920, 1080, false, true); echo "<br />";
recalc(3840, 2880, 1920, 1080, false, true); echo "<br />";
echo "<br/>";

recalc( 480,  360, 1440, 1080, true); echo "<br />";
recalc(1920, 1440, 1440, 1080, true); echo "<br />";
recalc( 480,  360, 1920, 1080, true); echo "<br />";
recalc(3840, 2880, 1920, 1080, true); echo "<br />";
echo "<br/>";

recalc( 480,  360, 1440, 1080, true, true); echo "<br />";
recalc(1920, 1440, 1440, 1080, true, true); echo "<br />";
recalc( 480,  360, 1920, 1080, true, true); echo "<br />";
recalc(3840, 2880, 1920, 1080, true, true); echo "<br />";
echo "<br/>";

This are the input, params and outputs used for testing. However some scenarios are still unwatched.

  • The 1st table states itself
  • the second contains the original sizes
  • the third the requested/target sizes
  • the last one is what 'we' expect for it to become

enter image description here

\$\endgroup\$
  • \$\begingroup\$ The third line of your function seems to contain a bug: $maxHeight = ($oldHeight < $reqWidth) ? $oldHeight : $reqHeight; should be $maxHeight = ($oldHeight < $reqHeight) ? $oldHeight : $reqHeight;. \$\endgroup\$ – KIKO Software Jun 28 '16 at 14:05
  • \$\begingroup\$ You're right, i've edited it. \$\endgroup\$ – IMarks Jun 28 '16 at 14:15
2
\$\begingroup\$

Complexity

It looks to me that even with this simple function, the complexity inside takes you for a ride. You don't have it under control. The reason is obvious: You try to do too much in one function.

If your program can set two flags, it can also choose between different functions: One for simple resizing, one for resizing while keeping the aspect ratio the same, etc. As you can guess there could be some overlap between these function. Therefore:

Classes

Classes are the ideal structures to put these multiple functions in, we call them 'methods', and let them share some common code. Let me give an example, that does some of the things that your function does, and some it doesn't:

function greatestCommonDivisor($int1,$int2)
// return greatest common divisor, euclidean algorithm
{
   if ($int2 == 0) return $int1;
   else return greatestCommonDivisor($int2,$int1 % $int2);
}


class ImageHandle
{
  private $outputSize;
  private $originalSize;

  public function __construct($width,$height)
  // constructor, set initial size
  {
    $this->setSize($width,$height);
    $this->originalSize = $this->getSize();
  }

  public function getSize()
  // get the Size of the rectangle
  {
    return $this->outputSize;
  }

  public function setSize($width,$height)
  // set the Size of the rectangle
  {
    $this->outputSize = ['width' => $width,'height' => $height];
    return $this;
  }

  public function resetSize()
  // go back to the orginal size
  {
    $this->outputSize = $this->originalSize;
    return $this;
  }

  private function computeAspectRatio($width,$height,$prefix = '')
  // return the aspect ratio as two numbers, with prefix
  {
    $divisor = greatestCommonDivisor($width,$height);
    $name    = ($prefix == '' ? 'aspect' : $prefix.'Aspect');
    return [$name.'X' => intdiv($width,$divisor),
            $name.'Y' => intdiv($height,$divisor)];
  }

  public function getAspectRatio()
  // return the aspect ratio as two numbers for set size
  {
    extract($this->getSize());
    return computeAspectRatio($width,$height);
  }

  public function setAspectRatio($newWidth,$newHeight)
  // set a new aspect ratio while keeping within the current width and height
  {
    extract($this->getSize());
    extract(computeAspectRatio($newWidth,$newHeight,'new'));
    if ($newAspectX > $aspectX) $height = $width*$newAspectY/$newAspectX;
                           else $width  = $height*$newAspectX/$newAspectY;
    return $this->setSize($width,$height);
  }

  public function rescaleSize($maxWidth,$maxHeight)
  // rescale, while keeping aspect ratio the same
  {
    extract($this->getSize());
    $newWidth  = $maxWidth;
    $newHeight = round($maxWidth*$height/$width);
    if ($newHeight > $maxHeight) {
      $newWidth  = round($newWidth*$maxHeight/$newHeight);
      $newHeight = $maxHeight;
    }
    return $this->setSize($newWidth,$newHeight);
  }

  public function echoInfo($title)
  // just here for this example, should be removed
  {
    echo $title.': ('.implode(',',$this->getSize()).
                 ') '.implode(':',$this->getAspectRatio()).'<br>';
    return $this;
  }

}

$test = [[ 480,  360, 1440, 1080],
         [1920, 1440, 1440, 1080],
         [ 480,  360, 1920, 1080],
         [3840, 2880, 1920, 1080]];

foreach ($test as $data)
{
  $image = new ImageHandle($data[0],$data[1]);
  $image->echoInfo('Orginal')
        ->setSize($data[2],$data[3])
        ->echoInfo('Set')
        ->resetSize()
        ->rescaleSize($data[2],$data[3])
        ->echoInfo('Rescale')
        ->resetSize()
        ->setAspectRatio($data[2],$data[3])
        ->echoInfo('Aspect Ratio');
  echo '<br>';
}

When ran this returns:

Orginal: (480,360) 4:3
Set: (1440,1080) 4:3
Rescale: (1440,1080) 4:3
Aspect Ratio: (480,360) 4:3

Orginal: (1920,1440) 4:3
Set: (1440,1080) 4:3
Rescale: (1440,1080) 4:3
Aspect Ratio: (1920,1440) 4:3

Orginal: (480,360) 4:3
Set: (1920,1080) 16:9
Rescale: (1440,1080) 4:3
Aspect Ratio: (480,270) 16:9

Orginal: (3840,2880) 4:3
Set: (1920,1080) 16:9
Rescale: (1440,1080) 4:3
Aspect Ratio: (3840,2160) 16:9

Ok, when you're not used to classes this might seem like killing a fly with an atomic bomb. I agree. But bear with me. I didn't call this class ImageHandle for nothing. You could extend this class to include other methods, mine are just examples, or an image loader and image saver, and much more.

However, notice the relative simplicity of each method: They don't get overburdened with functionality, so they stay relatively simple. Each one performs a single task.

Just like in your function, you could apply more than one method to change the size, but I found that to be a bit useless. Nevertheless, it will work.

PS: I intentionally left out the comments I normally put in my classes, this example is already quite long. You don't need them anyway, it isn't rocket science. :-)

PS2: I made this class slightly harder to understand, because I'm now using an array to store width and height. Some methods get very short, which is nice, but in others I use extract() to get at the data in the array. This is a method I often use. Arrays can be useful, but if you're not used to them they make it harder to read the code. Sorry for that.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Looks good, I'm not afraid of classes nor arrays. Exactly my code is used within a framework which enforces me to use classes. The code above is the basic version and I'm working on it to make it a class that suits me needs (logs, basic setups/config, maybe some design pattern if required/able) . As which this basic version I will also share the 'classed version' for easy implementation for others, however I will keep it more readable/understandable as yours (no offense intended). This however may take a few days before I have the free time. Anyway thanks for you feedback. ;) \$\endgroup\$ – IMarks Jun 29 '16 at 8:08

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.