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()