简体   繁体   中英

Create two equal height columns of random portrait and landscape images in PHP

Given a random number of portrait and landscape images, I am trying to write PHP code to output two columns of equal height:

  • By assigning the value "1" to landscape images and "2" to portraits, my idea is to divide the total images ($totalphotos) by two and then check to see that the image value ($photoval) doesn't exceed that number as it iterates through the loop.
  • For example, if you have 6 total images and the first column already has 2 landscape images and the third image it encouters is a portrait, it will re-order the array (array_splice) and move the portrait image down and then contiue to the next image.
  • If the output has already created two equal height columns (say 3 landscapes in the first column and 1 landscape + 1 portrait in the second column, the last image is dropped)

I'm not sure if my approach is correct to try and output the html during the loop, or if it would make more sense to analyze the array and re-order the images first and then create a second loop to output the html.

My code is a bit dirty and my "array_splice" method may not even be entirely correct. If you see a better way, feel free to scrap my code and show me something better! Any help is greatly appreciated!!

<?php

     $photoval = 0;
     $totalcolumns = 0;
     $totalphotos = count($photos);

     for ($counter = 0; $counter < $totalphotos; $counter++) :

            $photo = $photos[$counter];

            if ($photoval == 0) echo "        <div class=\"column\">\n";

            if ($photo['orientation'] == "portrait" && $photoval >= $totalphotos/2) {
            if ($counter == $totalphotos)
                    echo "        </div>\n";                
                    array_splice($photos, $counter, 1, array($photo));

                    continue;
            }
?>
     <div class="<? echo $photo['orientation'] ?> thumbnail">
          <a href="<?php echo $photo['link'] ?>">
               <img src="<?php if ($photo['orientation'] == "landscape") echo $photo['src']; else echo $photo['src_medium'];?>" alt="<? echo htmlentities($photo['caption'], ENT_QUOTES) ?>">
          </a>
     </div>
<?php
            if ($photoval >= $totalphotos/2 || $counter == $totalphotos) {
                echo "        </div>\n";
                $photoval = 0;
                $totalcolumns++;
                if ($totalcolumns == 2)
                    break;
            }

     endfor;
?>

Here is my solution:

<?php

/*
**  Simulated photo array for testing purposes, to be substituted with the real photo array
*/
$photos = array(
  array('orientation' => 'portrait' , 'link' => '#', 'src' => '', 'src_medium' => '', 'caption' => ''),
  array('orientation' => 'portrait' , 'link' => '#', 'src' => '', 'src_medium' => '', 'caption' => ''),
  array('orientation' => 'portrait' , 'link' => '#', 'src' => '', 'src_medium' => '', 'caption' => ''),
  array('orientation' => 'landscape', 'link' => '#', 'src' => '', 'src_medium' => '', 'caption' => ''),
  array('orientation' => 'landscape', 'link' => '#', 'src' => '', 'src_medium' => '', 'caption' => ''),
  array('orientation' => 'landscape', 'link' => '#', 'src' => '', 'src_medium' => '', 'caption' => ''),
  array('orientation' => 'landscape', 'link' => '#', 'src' => '', 'src_medium' => '', 'caption' => ''),
  array('orientation' => 'landscape', 'link' => '#', 'src' => '', 'src_medium' => '', 'caption' => ''),
  array('orientation' => 'landscape', 'link' => '#', 'src' => '', 'src_medium' => '', 'caption' => '')
);

$album = array(
  'portrait'  => array(),
  'landscape' => array()
);

foreach ($photos as $photo) {
  extract($photo);
  $album[$orientation][] = array(
    'orientation' => $orientation,
    'link'        => $link,
    'src'         => ($orientation == 'landscape') ? $src : $src_medium,
    'caption'     => htmlentities($caption, ENT_QUOTES)
  );
}

$columns = array(
  array(),
  array()
);

while (count($album['portrait']) >= 2) {
  if (count($album['landscape']) >= 2) {
    $columns[0][] = array_shift($album['portrait']);
    $columns[1][] = array_shift($album['landscape']);
    $columns[1][] = array_shift($album['landscape']);
  } else {
    $columns[0][] = array_shift($album['portrait']);
    $columns[1][] = array_shift($album['portrait']);
  }
}

while (count($album['landscape']) >= 2) {
  $columns[0][] = array_shift($album['landscape']);
  $columns[1][] = array_shift($album['landscape']);
}

?>
<!DOCTYPE html>
<html>
<head>
  <style>
  .column { width: auto; float: left; border: dashed 1px #666; padding: 10px }
  .column div:not(:last-child) { margin-bottom: 10px }
  .portrait { width: 200px; height: 200px; background-color: red; }
  .landscape { width: 200px; height: 95px; background-color: green; }
  </style>
</head>
<body>
  <?php foreach ($columns as $photos): ?>
  <div class="column">
    <?php foreach ($photos as $photo): ?>
    <div class="<?= $photo['orientation'] ?>">
      <!-- uncomment the following line when using the real photo array -->
      <!-- a href="<?= $photo['link'] ?>"><img src="<?= $photo['src'] ?>"> alt="<?= $photo['caption'] ?>"></a -->
    </div>
    <?php endforeach ?>
  </div>
  <?php endforeach ?>
</body>
</html>

* UPDATE *

Another solution:

<?php

/*
**  Create a random photo array
*/
$photos = array();
$totalphotos = rand(10, 30);
for ($i = 0; $i < $totalphotos; $i++) {
    $o = (rand(0, 1) == 1) ? 'portrait' : 'landscape';
    $photos[] = array('orientation' => $o, 'link' => '#', 'src' => '', 'src_medium' => '', 'caption' => '');
}

//----------------------------------
//--- Here starts the real stuff ---
//----------------------------------

/*
**  The "last" array contains the index of the last added
**  portrait and landscape image in each column of the
**  "album" array
*/
$last = array(
    'portrait' => array(0, 0), 
    'landscape' => array(0, 0)
);

/*
**  Add each photo to the lowest height column
*/
$album = array();
$len  = array(0, 0);
for ($i = 0; $i < $totalphotos; $i++) {
    $o = $photos[$i]['orientation'];
    $c = ($len[0] < $len[1]) ? 0 : 1;
    $len[$c] += ($o == 'portrait') ? 2 : 1;
    $album[$c][] = $i;
    $last[$o][$c] = count($album[$c]) - 1;
}

/*
**  If the columns heights are different,
**  remove the last portrait or landscape image
**  from the highest column
*/
$c = ($len[0] > $len[1]) ? 0 : 1;
$diff = abs($len[0] - $len[1]);
//echo "<pre>diff={$diff}</pre>\n";
if ($diff == 1) {
    unset($album[$c][$last['landscape'][$c]]);
} else if ($diff == 2) {
    unset($album[$c][$last['portrait'][$c]]);
}

?>
<!DOCTYPE html>
<html>
    <head>
        <style>
        .column { border: dashed 1px #666; width: 50px; padding: 0 10px 10px 10px; overflow: auto; float: left; }        
        .column div { margin: 10px 5px 0 0; }
        .portrait { width: 50px; height: 100px; background-color: red; }
        .landscape { width: 50px; height: 45px; background-color: green; }
        </style>
    </head>
    <body>
        <?php for ($c = 0; $c < 2; $c++) { ?>
        <div class="column">
            <?php foreach ($album[$c] as $p): ?>
            <div class="<?= $photos[$p]['orientation'] ?> thumbnail">
                <!--a href="<?= $photos[$p]['link'] ?>"><img src="<?= $photos[$p]['src'] ?>" alt="<?= $photos[$p]['caption'] ?>"></a-->
            </div>
            <?php endforeach ?>
        </div>
        <?php } ?>
    </body>
</html>

That's been a nice problem... well I've got a solution that seems to be as efficient as possible. First image objects are created on an initial stack, then moved to the left stack, as long as there is place on it, then to the right stack. If a portrait(2) shall be moved onto the left stack and there is only place for a landscape(1), a landscape is move to a trash stack and the portrait is move to the left one. Same logic for the right side.

Just save this code and play around with it... (it's self-posting)

<style type="text/css">
    * { margin: 0; padding: 0; }
    ul { list-style-type: none; width: 100px; }
    b { color: white; }
    .landscape { width: 100px; height: 50px; background-color: #f00; }
    .portrait { width: 50px; height: 100px; background-color: #00f; }
</style>

<form action="<?php echo $_SERVER['REQUEST_URI']; ?>" method="get">
    number of images to simulate: <input type="text" name="nimgs" value="<?php if(isset($_REQUEST['nimgs'])) echo intval($_REQUEST['nimgs']); else echo 10; ?>" /><br/>
    <input type="submit">
</form><br/>
<hr/>

<?php

    class ImageStack
    {
        public $images, $height;
        function __construct()
        {
            $this->images = array();
            $this->height = 0;
        }
        function push($image)
        {
            if( $image === false ) return; # catch failed calls to pop()
            $this->height += $image['height'];
            array_push( $this->images, $image );
        }
        function shift()
        {
            $image = array_shift( $this->images );
            $this->height -= $image['height'];
            return $image;
        }
        function total()
        {
            return $this->height;
        }
        function has($size)
        {
            foreach( $this->images as $i )
            {
                if( $i['height'] == $size ) return true;
            }
            return false;
        }
        function move( $size, $to, $max )
        {
            foreach( $this->images as $key => $value )
            {
                if( $value['height'] == $size )
                {
                    if( $to->total() + $size <= $max )
                    {
                        $this->height -= $size;
                        $to->push( $value );
                        unset( $this->images[$key] );
                    }
                    return;
                }
            }
        }
        function output( $msg )
        {
            echo '<ul style="float: left; margin-left: 10px; background-color: #000;"><b>'.$msg.'&nbsp;</b>';
            foreach( $this->images as $image )
            {
                echo "<li class='".($image['height'] == 1 ? 'landscape' : 'portrait')."'>$image[src]</li>";
            }
            echo '</ul>';
        }
    }

    ### create the random images ###
    $nimgs = intval($_REQUEST['nimgs']);

    $images = new ImageStack;
    for($i=0; $i<$nimgs; $i++)
    {
        $images->push(  array( 'height' => 1+rand()%2, 'src' => "img $i" )  );
    }

    ### write the first column ###
    $images->output('All: '.$images->total());

    ### initialization ###
    $half  = floor($images->total() / 2);
    $left  = new ImageStack;
    $right = new ImageStack;
    $temp  = new ImageStack;

    ### if we got an odd total() of image height, remove a single 1 to temp ###
    if( $images->total() % 2 )
    {
        $images->move(1, $temp, 3*$half); # sad but true: this moves the first one to temp, better would be the last
    }

    ### while we got images on the initial stack ###
    while( $images->total() )
    {
        $next = $images->shift();
        if( $left->total() + $next['height'] <= $half )             # enough space @ left
        {
            $left->push( $next );
        }
        else
        {
            if( $left->total() < $half && $left->has(1) )           # enough space @ left if we move a 1 to temp
            {
                $left->move( 1, $right, $half );
                $left->push( $next );
            }
            else
            {
                if( $right->total() + $next['height'] <= $half )    # enough space @ right
                {
                    $right->push( $next );
                }
                else
                {
                    if( $right->total() < $half && $right->has(1) ) # enough space @ right if we move a 1 to temp
                    {
                        $right->move(1, $temp, 3*$half);
                        $right->push( $next );
                    }
                    else                                            # nowhere enough space left, except @ temp
                    {
                        $temp->push( $next );
                    }
                }
            }
        }
    }

    $left->output('Left: '.$left->total());
    $right->output('Right: '.$right->total());
    $temp->output('Ignore: '.$temp->total());   
?>

Split into small chunks

The easiest way to address any problem - is to look at each bit individually, see below code:

/**
 * Turn the array of photos into 2 equal height columns
 *
 * @param array photos - array of photos
 * @return string
 */
function printPhotos($photos) {
    $cells = buildCells($photos);
    return renderColumns($cells);
}

/**
 * Take the input array, and build an indexed array
 *
 * Use variable references to construct portrait and landscape arrays,
 * and maintain an ordered list such that the original order (after
 * accounting for the cell logic) is maintained.
 * If at the end there is one portrait image on its own - delete it.
 *
 * @param array photos - array of photos
 * @return array
 */
function buildCells($photos) {
    $return = array(
        'ordered' => array(),
        'landscape' => array(),
        'portrait' => array()
    );
    $i = 0;

    foreach($photos as $photo) {
        unset($cell);
        $orientation = $photo['orientation'];

        if ($orientation === 'portrait') {
            if (empty($return['portrait'][$i])) {
                $cell = array();
                $return['portrait'][$i] =& $cell;
                $return['ordered'][] =& $cell;
            } else {
                $cell =& $return['portrait'][$i];
            }
            $cell[] = $photo;

            if (count($cell) === 2) {
                $i++;
            }
        } else {
            $cell = array($photo);
            $return['landscape'][] =& $cell;
            $return['ordered'][] =& $cell;
        }

    }

    if (count($return['portrait'][$i]) === 1) {
        $return['portrait'][$i] = null;
        $return['portrait'] = array_filter($return['portrait']);
        $return['ordered'] = array_filter($return['ordered']);
    }

    return $return;
}

/**
 * Convert the output of buildCells into html
 *
 * @param array cells - indexed array of cells
 * @return string column html
 */
function renderColumns($cells) {
    $orderedCells = renderCells($cells);

    $cellsPerColumn = (int)(count($orderedCells) / 2);
    $columns = array_slice(array_chunk($orderedCells, $cellsPerColumn), 0, 2);

    $return = '';

    foreach($columns as $cellsInColumn) {
        $return .= "<div class=\"column\">\n";
        $return .= implode('', $cellsInColumn);
        $return .= "</div>\n";
    }

    return $return;
}

/**
 * Process portrait and landscape photo-cells
 *
 * Converts the array representation of cells into html, and returns
 * The cells in presentation order
 *
 * @param array cells - indexed array of cells
 * @return array
 */
function renderCells($cells) {
    foreach(array('landscape', 'portrait') as $orientation) {
        foreach($cells[$orientation] as &$cell) {
            $cell = renderCell($cell, $orientation);
        }
    }

    return $cells['ordered'];
}

/**
 * For each photo in the cell - turn it into html
 *
 * @param array cell - array of photo(s)
 * @param string orientation
 * @return string
 */
function renderCell(&$cell, $orientation) {
    $return = "\t<div class=\"cell\">\n";

    foreach($cell as $photo) {
        $return .= renderPhoto($photo, $orientation);
    }

    $return .= "\t</div>\n";

    return $return;
}

/**
 * Convert the photo into a html string
 *
 * @param array photo
 * @param string orientation
 * @return string
 */
function renderPhoto($photo, $orientation) {
    if ($orientation === 'landscape') {
        $src = $photo['src'];
    } else {
        $src = $photo['src_medium'];
    }
    $caption = htmlentities($photo['caption'], ENT_QUOTES);

    $return = "\t\t<div class=\"$orientation thumbnail\">\n";
    $return .= "\t\t\t<a href=\"{$photo['link']}\"><img src=\"$src\" alt=\"$caption\"></a>\n";
    $return .= "\t\t</div>\n";

    return $return;
}

By creating functions which do one thing - it makes it a lot easier to verify that the code does what you are expecting. There are a number of requirements in the question which, if written as a single chunk of code, are difficult to verify are met.

The main function is buildCells .

Example

Given this example data:

$data = array(
    array('src' => 'x', 'src_medium' => 'y', 'orientation' => 'landscape', 'link' => 'z', 'caption' => 'one'),
    array('src' => 'x', 'src_medium' => 'y', 'orientation' => 'portrait', 'link' => 'z', 'caption' => 'two'),
    array('src' => 'x', 'src_medium' => 'y', 'orientation' => 'portrait', 'link' => 'z', 'caption' => 'three'),
    array('src' => 'x', 'src_medium' => 'y', 'orientation' => 'portrait', 'link' => 'z', 'caption' => 'four'),
    array('src' => 'x', 'src_medium' => 'y', 'orientation' => 'landscape', 'link' => 'z', 'caption' => 'five'),
    array('src' => 'x', 'src_medium' => 'y', 'orientation' => 'portrait', 'link' => 'z', 'caption' => 'six'),
    array('src' => 'x', 'src_medium' => 'y', 'orientation' => 'portrait', 'link' => 'z', 'caption' => 'seven')
);

echo printPhotos($data);

The output of the code as included in the question is:

<div class="column">
    <div class="cell">
        <div class="landscape thumbnail">
            <a href="z"><img src="x" alt="one"></a>
        </div>
    </div>
    <div class="cell">
        <div class="portrait thumbnail">
            <a href="z"><img src="y" alt="two"></a>
        </div>
        <div class="portrait thumbnail">
            <a href="z"><img src="y" alt="three"></a>
        </div>
    </div>
</div>
<div class="column">
    <div class="cell">
        <div class="portrait thumbnail">
            <a href="z"><img src="y" alt="four"></a>
        </div>
        <div class="portrait thumbnail">
            <a href="z"><img src="y" alt="six"></a>
        </div>
    </div>
    <div class="cell">
        <div class="landscape thumbnail">
            <a href="z"><img src="x" alt="five"></a>
        </div>
    </div>
</div>

Some notes/analysis, follow.

2 equal-height columns

The method renderColumns takes a nested array of the photo data and first converts it into a flat array of html strings. This method assumes that each html string is the same dimensions (1 landscape image, or 2 portrait images side by side). If there are an odd number of html snippets - it'll drop the last one.

No lone portrait images

The method buildCells checks if the last portrait image is on it's own, and if it is - removes it. If this isn't exactly what you want - just remove the lines right before the return statement that are deleting the lone portrait image.

Extra markup for "cells"

You may find it easier to style the result - having some consistent html wrapping your 2 images - for this reason I added some markup for a cell div: div.column > div.cell > div.thumbnail > img . If that's not desired - again, easy to remove.

Note that unless there is more markup in div.thumbnail than in the question it's not necessary.

OR do it with js

There are two js solutions, each by the same author, that do something similar with js to what you're doing with php: masonry and isotope . With js it's a lot easier to account for images of various (not just two) sizes, and any other rendering quirks that cause the final html to be different sizes than anticipated.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM