from __future__ import division ''' What follows is the original comment for the PHP class. Python-specific notes can be found below, under the heading PYTHON VERSION. magazinelayout.class.php Introduction ============ A class for creating magazine-like layouts for images. A Magazine-like layout arranges the images at different sizes so that all images fit within a defined "square box". This can be an attractive way of arranging images when you are dealing with user-uploaded images, or don't have a graphic designer handy to arrange and resize them in photoshop. Purpose ======= The obvious use for this script is anywhere where more than one user submitted image needs to be presented in a HTML page. I'm thinking product databases, forum image uploads, random image rotations, etc etc. Once you have 10 or so images, you are better off using an AJAX based image gallery, but this script will fill the gap nicely up till that point. Layouts ======= The layouts that are used depend on the number of landscape and portrait images. For example, if we are given a portrait and 2 landscapes, the layout will appear as follows (different numbers represent different images)... 11113333 11113333 22223333 22223333 With 3 landscapes, the layout may appear as such... 11112222 11112222 33333333 33333333 With 2 portraits, 1 landscape we use 11122222222333 or 111222 11122222222333 111222 11122222222333 333333 333333 With 3 portraits, we could use either 111222333 or 11333 111222333 11333 111222333 22333 22333 If you have 4 images to display, this class will use any of the following... 111222 112233 111444 111222 112233 222444 333444 444444 333444 333444 444444 Logic ===== The logic behind these calculations are based on algebra - yes, x + y = z (but a little more complicated). I have attempted to clearly document all calculations, however you will find the tools at http://www.quickmath.com/ very useful if you aren't a mathematics expert (I'm certainly not one). Requirements ============ -A PHP 4.3.x server with GD2.x extension enabled - most PHP shared hosting is suitable -An image resizing script - I have included a very simple one with this bundle Usage ===== //include the class file require_once('magazinelayout.class.php') //Define the width for the output area (pixels) $width = 600 //Define padding around each image - this *must* be included in your stylesheet (pixels) $padding = 3 //Define your template for outputting images $template = "\"\"" //Don't forget to escape the & //create a new instance of the class $mag = new magazinelayout($width,$padding,$template) //Add the images in any order $mag->addImage('landscape1.jpg') $mag->addImage('portrait1.jpg') $mag->addImage('landscape2.jpg') //display the output echo $mag->getHtml() Template ======== A different tag will be required for different installations, depending mainly on the image script used. Variables: [size] = The size of the image, either 500, h500 or w500 [file] = The filename of the image, eg images/image1.jpg The default is... Using Apache's mod_rewrite, a better format might be... Note your script and server must be configured for this, details of this are out of the scope of this script A static looking image URL is better because Google will usually ignore dynamic looking images (they look like PHP scripts, not images) CSS === The following CSS is required for padding to work correctly... .magazine-image { background: #fff border: 1px #eee solid } .magazine-image img { padding: 0px background: #fff margin: 2px border: 1px #eee solid } Padding ======= Including padding between images was the most complicated part of this script. On the more complex layouts, the equations double in complexity once padding is added. Padding is implemented as x pixels gap between the images - x is defined when the class is created. The padding you specify *must* be reflected in the stylesheet you use, or the layout will not look right. Because IE deals with padding incorrectly, padding has been implemented as "margin" instead. If a padding of 3 is specified in the PHP class, then the CSS class for ".magazine-image img" should reflect a margin of 3px or a margin o 2px + border of 1px. Do not specify padding on the image unless you are prepared to hack the PHP code. Rounding ======== Almost all of the calculations are done using floating point numbers. Because HTML required whole numbers, the numbers need to be rounded (down) before outputting. On some examples, this makes no difference. On others, the 1 or 2 pixels worth of rounding is noticeable. Would welcome any suggestions on this issue for those who want pixel perfect layouts. Restrictions ============ -There is a current limit of 8 images. This can easily be extended. -Images must be a reasonable quality. Images that are too small are stretched which doesn't look good. -The included image resizing script is very basic. I recommend using a script which caches output. The image resizing script that is right for you will depend on your server configuration and is out of the scope of this class. To Do ===== There are several obvious improvements that can be made to this script. These include... -Adding code for more than 8 images -Configuring so that low-res images are always shown in the small spots -Allow positioning of images (though this does defeat the purpose) -Include an all-purpose image resizing script with static looking URLs and Image caching -Full testing for version 1 release -Rounding issue explained above Copyright ========= This file may be used and distributed subject to the LGPL available from http://www.fsf.org/copyleft/lgpl.html If you find this script useful, I would appreciate a link back to http://www.ragepank.com or simply an email to let me know you like it :) You are free (and encouraged) to modify or enhance this class - if you do so, I would appreciate a copy of the changes if it's not too much trouble. Please keep this copyright statement intact, and give fair credit on any derivative work. About the Author ================ Harvey Kane is a PHP Web developer living and working in Auckland New Zealand. He is interested in developing "best practice" websites, and especially interested in using PHP to automate this process as much as possible. Harvey works as a freelance developer doing CMS websites and SEO under the umbrella of www.harveykane.com, and publishes SEO Articles and tools at www.ragepank.com Ragepank.com ============ www.ragepank.com is a source of original SEO Articles and tools - PHP based techniques for improving search engine positions, and good practice for web development. Support ======= I am happy to help with support and installation, so long as all documentation and forum threads are read first. You can contact me at info@ragepank.com Thanks ====== Thanks to Alexander Burkhardt (www.alex3d.de) for the use of the demo images. The images were taken on the lovely Hokianga Harbour in Northland New Zealand. @version 0.9 @copyright 2006 Harvey Kane @author Harvey Kane info@ragepank.com PYTHON VERSION Important changes from the PHP version: * The imagetemplate string (for the tags) should be the format for Python's % operator, e.g.: '' * The getHtml method has been renamed getHTML. ''' import Image LANDSCAPE = 'landscape' PORTRAIT = 'portrait' class Error(Exception): pass class InvalidSizeError(Error): pass class NonImageError(Error): pass class EmptyImageError(Error): pass class MagazineLayout(object): def __init__(self, maxwidth=600, padding=3, imagetemplate=None): self.images = [] self._numimages = 0 # Necessary? self._fullwidth = maxwidth self._padding = padding self._imagetemplate = (imagetemplate or '') def _isImage(self, filename): "Returns true if filename ends in an image extension." filename = filename.lower() return (filename.endswith("jpg") or filename.endswith("jpeg") or filename.endswith("png") or filename.endswith("gif") ) def addImage(self, filename, url=None, size=None): "Adds an image to the layout." if url is None: url = filename if not self._isImage(filename): raise NonImageError(repr(filename) + " is not an image filename.") if size is not None: w, h = size else: # Get the dimensions of the image image = Image.open(filename) w, h = image.size # Reject empty images if ((h == 0) or (w == 0)): raise EmptyImageError("Bad image size.") # Find the ratio of width:height ratio = w / h # Find the format based on the dimensions format = (w > h) and LANDSCAPE or PORTRAIT # Save all image details to a dict self.images.append({ 'filename': filename, 'url': url, 'format': format, 'ratio': ratio, 'w': w, # Not currently used 'h': h # Not currently used }) return True def getImageTag(self, index, maxsize=None, maxwidth=None, maxheight=None): """ Replaces variables into the supplied image template. One of maxsize, maxwidth, or maxheight must be specified. Override this method to change the way that these values are passed to the image resizing script. """ if maxsize is not None: size = maxsize elif maxwidth is not None: size = 'w%d' % maxwidth elif maxheight is not None: size = 'h%d' % maxheight else: raise InvalidSizeError("Please supply a dimension for the image.") image = self.images[index]['url'] return self._imagetemplate % dict(size=size, image=image) # IMAGE LAYOUTS # ============= # These layouts are coded based on the number of images. # Some fairly heavy mathematics is used to calculate the image sizes # and the excellent calculators at http://www.quickmath.com/ were # very useful. Each of these layouts outputs a small piece of HTML # code with the images and a containing div around each. def get1a(self, i1): ''' 111 or 1 1 ''' size = self._fullwidth - (self._padding * 2) return ('
' + self.getImageTag(i1, maxsize=size) + '
' ) def get2a(self, i1, i2): ''' 1122 Equation: t = 4p + ha + hb Variable: h ''' a = self.images[i1]['ratio'] b = self.images[i2]['ratio'] t = self._fullwidth p = self._padding h1 = (4 * p - t) // (-a - b) return ''.join([ '
', self.getImageTag(i1, maxheight=h1), '
' '
', self.getImageTag(i2, maxheight=h1), '
\n' ]) def get3a(self, i1, i2, i3): "1223" a = self.images[i3]['ratio'] b = self.images[i1]['ratio'] c = self.images[i2]['ratio'] t = self._fullwidth p = self._padding # Enter the following data at # http://www.hostsrv.com/webmab/app1/MSP/quickmath/02/pageGenerate?site=quickmath&s1=equations&s2=solve&s3=advanced#reply # EQUATIONS # t = 6p + ah + bh + ch # VARIABLES # h h1 = (6 * p - t) // (-a -b -c) return ''.join([ '
', self.getImageTag(i1, maxheight=h1), '
\n' '
', self.getImageTag(i3, maxheight=h1), '
\n' '
', self.getImageTag(i2, maxheight=h1), '
\n' ]) def get3b(self, i1, i2, i3): ''' 1133 2233 ''' a = self.images[i3]['ratio'] b = self.images[i1]['ratio'] c = self.images[i2]['ratio'] t = self._fullwidth p = self._padding # Enter the following data at http://www.hostsrv.com/webmab/app1/MSP/quickmath/02/pageGenerate?site=quickmath&s1=equations&s2=solve&s3=advanced#reply # EQUATIONS # x/a = w/b + w/c + 2p # w+x+4p = t # VARIABLES # w # x # width of left column with 2 small images w1 = -( (2 * a * b * c * p + 4 * b * c * p - b * c * t) // (a * b + c * b + a * c) ) # width of right column with 1 large image w2 = ((a * (-4 * b * p + 2 * b * c * p - 4 * c * p + b * t + c * t)) // (a * b + c * b + a * c)) return ''.join([ '
', self.getImageTag(i3, maxwidth=w2), '
\n' '
', self.getImageTag(i1, maxwidth=w1), '
\n' '
', self.getImageTag(i2, maxwidth=w1), '
\n' ]) def get4a(self, i1, i2, i3, i4): "1234" a = self.images[i1]['ratio'] b = self.images[i2]['ratio'] c = self.images[i3]['ratio'] d = self.images[i4]['ratio'] t = self._fullwidth p = self._padding # Enter the following data at # http://www.hostsrv.com/webmab/app1/MSP/quickmath/02/pageGenerate?site=quickmath&s1=equations&s2=solve&s3=advanced#reply # EQUATIONS # t = 6p + ah + bh + ch + dh # VARIABLES # h h1 = (8 * p - t) // (-a -b -c -d) return ''.join([ "
", self.getImageTag(i1, maxheight=h1), "
\n" "
", self.getImageTag(i2, maxheight=h1), "
\n" "
", self.getImageTag(i3, maxheight=h1), "
\n" "
", self.getImageTag(i4, maxheight=h1), "
\n" ]) def get4b(self, i1, i2, i3, i4): ''' 11444 22444 33444 ''' a = self.images[i4]['ratio'] b = self.images[i1]['ratio'] c = self.images[i2]['ratio'] d = self.images[i3]['ratio'] t = self._fullwidth p = self._padding # Enter the following data at http://www.hostsrv.com/webmab/app1/MSP/quickmath/02/pageGenerate?site=quickmath&s1=equations&s2=solve&s3=advanced#reply # EQUATIONS # x/a = w/b + w/c + 2p # w+x+4p = t # VARIABLES # w # x # width of left column with 2 small images w1 = -( (4 * a * b * c * d * p + 4 * b * c * d * p - b * c * d * t) // (a * b * c + a * d * c + b * d * c + a * b * d) ) # width of right column with 1 large image w2 = -((-4 * p - (-(1/c) -(1/d) -(1/b)) * (4 * p - t) ) // ((1/b) + (1/c) + (1/d) + (1/a)) ) return ''.join([ '
', self.getImageTag(i4, maxwidth=w2), '
\n' '
', self.getImageTag(i1, maxwidth=w1), '
\n' '
', self.getImageTag(i2, maxwidth=w1), '
\n' '
', self.getImageTag(i3, maxwidth=w1), '
\n' ]) def getHTML(self): # Sort the images array landscape first, then portrait #this.images = $this->_transpose($this->images) #array_multisort(self.images['format'], SORT_STRING, SORT_ASC, self.images['url'], self.images['ratio']) #$this->images = $this->_transpose($this->images) def sort(x, y): return cmp(x['format'], y['format']) or cmp(x['ratio'], y['ratio']) self.images.sort(cmp=sort) # Profile explains the makeup of the images (landscape vs # portrait) so we can use the best layout eg. LPPP or LLLP profile = '' for image in self.images: profile += (image['format'] is LANDSCAPE) and 'L' or 'P' # Open the containing DIV html = [ '
\n' % self._fullwidth ] n = len(self.images) if n == 1: html.append(self.get1a(0)) elif n == 2: html.append(self.get2a(0, 1)) elif n == 3: if profile == 'LLL': html.append(self.get3b(0, 1, 2)) else: html.append(self.get3b(0, 1, 2)) elif n == 4: if profile == 'LLLP': html.append(self.get4b(0, 1, 2, 3)) elif profile == 'LPPP': html.append(self.get3a(1, 2, 3)) html.append(self.get1a(0)) else: # LLLL LLPP PPPP html.append(self.get2a(2, 0)) html.append(self.get2a(1, 3)) elif n == 5: if profile == 'LLLLL': html.append(self.get3a(0, 1, 2)) html.append(self.get2a(3, 4)) elif profile == 'LLLLP': html.append(self.get3b(0, 1, 4)) html.append(self.get2a(2, 3)) elif profile == 'LLLPP': html.append(self.get3b(0, 1, 4)) html.append(self.get2a(2, 3)) elif profile == 'LLPPP': html.append(self.get3b(2, 3, 4)) html.append(self.get2a(0, 1)) elif profile == 'LPPPP': html.append(self.get3b(2, 3, 4)) html.append(self.get2a(0, 1)) elif profile == 'PPPPP': html.append(self.get2a(4, 0)) html.append(self.get3a(1, 2, 3)) elif n == 6: if profile == 'LLLLLL': html.extend(( self.get2a(0, 1), self.get2a(2, 3), self.get2a(4, 5) )) elif profile == 'LLLLLP': html.append(self.get4b(0, 1, 2, 5)) html.append(self.get2a(3, 4)) elif profile == 'LLLLPP': html.append(self.get3b(0, 1, 4)) html.append(self.get3b(2, 3, 5)) elif profile == 'LLLPPP': html.append(self.get3b(0, 1, 5)) html.append(self.get3b(2, 3, 4)) elif profile == 'LLPPPP': html.append(self.get3b(0, 2, 4)) html.append(self.get3b(1, 3, 5)) elif profile == 'LPPPPP': html.append(self.get3b(0, 1, 5)) html.append(self.get3a(2, 3, 4)) elif profile == 'PPPPPP': html.append(self.get3a(3, 4, 5)) html.append(self.get3a(0, 1, 2)) elif n == 7: if profile == 'LLLLLLL': html.append(( self.get3a(0, 1, 2), self.get2a(3, 4), self.get2a(5, 6) )) elif profile == 'LLLLLLP': html.append(self.get4b(0, 1, 2, 6)) html.append(self.get3a(3, 4, 5)) elif profile == 'LLLLLPP': html.append(self.get4b(0, 1, 2, 5)) html.append(self.get3b(3, 4, 6)) elif profile == 'LLLLPPP': html.append(self.get3b(0, 1, 5)) html.append(self.get4b(2, 3,4, 6)) elif profile == 'LLLPPPP': html.append(self.get3b(0, 1, 5)) html.append(self.get4b(2, 3, 4, 6)) elif profile == 'LLPPPPP': html.append(( self.get3a(4, 5, 6), self.get2a(0, 1), self.get2a(2, 3) )) elif profile == 'LPPPPPP': html.append(self.get3a(0, 1, 2)) html.append(self.get4b(3, 4, 5, 6)) elif profile == 'PPPPPPP': html.append(self.get4a(0, 1, 2, 3)) html.append(self.get3b(4, 5, 6)) elif n >= 8: # Note this code is applied for 8 or more images - any # images over 8 are ignored. Adding support for more than 8 # images would be easy, but the layouts do start losing # their effect as more images are added. if profile == 'LLLLLLLL': html.append(( self.get3a(0,1,2), self.get2a(3,4), self.get3a(5,6,7) )) elif profile == 'LLLLLLLP': html.append(self.get4b(0,1,2,7)) html.append(self.get2a(3,4)) html.append(self.get2a(5,6)) elif profile == 'LLLLLLPP': html.append(self.get4b(0,1,2,6)) html.append(self.get4b(3,4,5,7)) elif profile == 'LLLLLPPP': html.append(self.get4b(0,1,2,6)) html.append(self.get4b(3,4,5,7)) elif profile == 'LLLLPPPP': html.append(self.get4b(0,1,2,6)) html.append(self.get4b(3,4,5,7)) elif profile == 'LLLPPPPP': html.append(self.get3a(4,5,6)) html.append(self.get2a(0,1)) html.append(self.get3a(2,3,7)) elif profile == 'LLPPPPPP': html.append(( self.get3b(5,6,7), self.get2a(0,1), self.get3b(2,3,4) )) elif profile == 'LPPPPPPP': html.extend(( self.get3b(5,6,7), self.get2a(0,1), self.get3b(2,3,4) )) elif profile == 'PPPPPPP': html.append(self.get4a(0,1,2,3)) html.append(self.get4a(4,5,6,7)) else: html.append(( self.get3b(5,4,7), self.get2a(1,0), self.get3b(2,3,6) )) # Close the containing DIV html.append('
\n
\n') return ''.join(html) # End of class def test1(): global res ml = MagazineLayout(maxwidth=200) ml.addImage("file1.jpg", size=(100, 200)) ml.addImage("file2.png", size=(200, 100)) ml.addImage("file3.png", size=(150, 100)) res = ml.getHTML()