Each instance of a FormPhotoSet contains all
the images that have the same inverted English name.
# - - - - - c l a s s F o r m P h o t o S e t - - - - -
class FormPhotoSet:
"""Container for photos that have birds with a given English name.
Exports:
FormPhotoSet ( birdId ):
[ birdId is a bird form code as an abbr.BirdID ->
return a new, empty FormPhotoSet for birdId ]
.birdId: [ as passed to constructor; read-only ]
.addArchImage ( archImage ):
[ archImage is a photo catalog entry as an
archindex.ArchImage instance ->
self := self with archImage added ]
.genArchImages():
[ generate the ArchImage instances in self, in
descending order by image size, with catalog number
as a secondary key ]
.pathName():
[ returns the relative path name to the page that
displays this form ]
.buildPage():
[ form page for self := content displaying
thumbnails and catalog data for self ]
One internal data structure suffices to hold all the ArchImage instances of the set. One of the
author's standard Python tricks is to keep a set of values
in a dictionary, and then structure the key so that, when
sorted, it produces the values in the desired order.
As discussed in Section 3.2, “The form page”, the principal sort order of images is in descending order by image area. In case two images happen to have the same area, their catalog number is used as a tie-breaker. Hence, the dictionary key is a tuple of two values:
(-area,catNo)
where -area is the image's area in square
millimeters as a float, negated, and catNo is the
image's catalog number.
State/Invariants:
.__sizeMap:
[ a dictionary whose values are the ArchImage instances
contained in self, and each key is a 2-tuple whose
first element is the image's area in square mm as a
float, negated, and the second element is the
image's catalog number ]
"""
All the constructor does is to record its arguments
and initialize the .__sizeMap dictionary.
# - - - F o r m P h o t o S e t . _ _ i n i t _ _ - - -
def __init__ ( self, birdId ):
"""Constructor."""
self.birdId = birdId
self.__sizeMap = {}
To add a new ArchImage object to this
FormPhotoSet instance, we construct the
2-tuple used as a key to self.__sizeMap,
then store the ArchImage under that key.
There shouldn't be any duplications, but let's check
anyway.
# - - - F o r m P h o t o S e t . a d d A r c h I m a g e - - -
def addArchImage ( self, archImage ):
"""Add a new archived, cataloged image to self.
"""
#-- 1 --
# [ area := number of pixels in archImage ]
area = archImage.wide * archImage.high
#-- 2 --
# [ key := ( -area, catalog number from archImage ) ]
key = ( -area, archImage.original.catNo )
#-- 3 --
if self.__sizeMap.has_key ( key ):
raise KeyError, ( "Duplicate form photo: area %dpx, "
"catalog number %s" %
(area, archImage.original.catNo) )
else:
self.__sizeMap [ key ] = archImage
This method generates all our contained ArchImage instances. It uses the standard
Python trick of setting up a dictionary key so that it
sorts in the desired order. In our case, the order is
descending order by area, with catalog number as a
tiebreaker.
# - - - F o r m P h o t o S e t . g e n A r c h I m a g e s - - -
def genArchImages ( self ):
"""Generate contained images.
"""
keyList = self.__sizeMap.keys()
keyList.sort()
for key in keyList:
yield self.__sizeMap[key]
raise StopIteration
This method returns the path name of this form's form
page, relative to the index page. Python's os.path.join() function is used to mate the
directory and file names. The file name is based on the bird
code from self.birdId, but we must
sanitize it by translating the relationship codes. The
class variable relMap maps those codes
onto strings acceptable for use in file names.
# - - - F o r m P h o t o S e t . p a t h N a m e - - -
relMap = { abbrModule.REL_HYBRID: "-x-",
abbrModule.REL_PAIR: "-o-" }
def pathName ( self ):
"""Returns the relative path name of self's form page.
"""
#-- 1 --
# [ if self.birdId.rel is None ->
# basename := self.birdId.abbr
# else ->
# basename := self.birdId.abbr +
# (self.birdId.rel, translated using self.relMap) +
# self.birdId.abbr2 ]
if self.birdId.rel is None:
basename = self.birdId.abbr.rstrip().lower()
else:
cleanRel = self.relMap [ self.birdId.rel ]
basename = ( "%s%s%s" %
(self.birdId.abbr.rstrip().lower(), cleanRel,
self.birdId.abbr2.rstrip().lower()) )
To distinguish questionable forms, we add "-q" to the end in that case.
#-- 2 --
if self.birdId.q: suffix = "-q"
else: suffix = ""
For the name of the forms subdirectory, see Section 9.2, “FORM_SUBDIR: Form subdirectory name”.
#-- 2 --
# [ return FORM_SUBDIR + basename + ".html" ]
path = os.path.join ( FORM_SUBDIR, basename )
return "%s%s.html" % (path, suffix)
This method is the driver for the construction of the
XHTML page representing the forms in this FormPhotoSet.
# - - - F o r m P h o t o S e t . b u i l d P a g e - - -
def buildPage ( self ):
"""Builds the XHTML page for self.
"""
For the URL of the stylesheet, see Section 9.1, “CSS_STYLESHEET: Stylesheet name”.
#-- 1 --
# [ page := a new, generic XHTML page with self's title
# body := the body element of that page ]
titleText = ( "Shipman's bird photo index: %s" %
self.birdId.engComma() )
page, body = webPage ( titleText, CSS_STYLESHEET )
The body of the page is mainly a table. See Section 12.6, “formSet.__buildTable(): Build the
image table”.
#-- 2 --
# [ body := body with a table added containing
# thumbnails and catalog data from self ]
self.__buildTable ( body )
The page is now complete. Create the file and serialize the page to it.
#-- 3 --
# [ if self's page can be created new ->
# pagePath := path to self's file
# pageFile := a new, empty file ]
pagePath = self.pathName()
pageFile = open ( pagePath, "w" )
#-- 4 --
# [ pageFile +:= page, serialized ]
page.write ( pageFile )
pageFile.close()
This method builds the table that holds the thumbnails and image data. For the XHTML, see Section 4.3, “XHTML for the form page”.
# - - - F o r m P h o t o S e t . _ _ b u i l d T a b l e
def __buildTable ( self, body ):
"""Build the XHTML table.
[ body := body with a table added containing
thumbnails and catalog data from self ]
"""
#-- 1 --
# [ body := body with a new table element
# table := that table element ]
table = xc.Element ( body, "table", border="4",
cellspacing="4" )
#-- 2 --
# [ table := table with a new colgroup added defining
# the table columns ]
colgroup = xc.Element ( table, "colgroup" )
xc.Element ( colgroup, "col", align="center" )
xc.Element ( colgroup, "col", align="right", valign="top" )
xc.Element ( colgroup, "col", align="left", valign="top" )
#-- 3 --
# [ table := table with a thead added containing the
# column headings ]
thead = xc.Element ( table, "thead" )
headRow = xc.Element ( thead, "tr" )
head1 = xc.Element ( headRow, "th", align="center" )
xc.Text ( head1, "Thumbnail" )
head2 = xc.Element ( headRow, "th", align="center" )
xc.Text ( head2, "Size" )
head3 = xc.Element ( headRow, "th", align="center" )
xc.Text ( head3, "Data" )
#-- 4 --
# [ table := table with a tbody element added
# tbody := that tbody element ]
tbody = xc.Element ( table, "tbody" )
Finally, we add a row to the table for each contained
ArchImage object.
#-- 5 --
# [ tbody := tbody with rows added representing the
# ArchImages in self, in key order ]
for archImage in self.genArchImages():
#-- 5 body --
# [ archImage is an archx.ArchImage ->
# tbody := tbody with a row added representing
# archImage ]
self.__addRow ( tbody, archImage )
This method translates one ArchImage
instance into a table row.
# - - - F o r m P h o t o S e t . _ _ a d d R o w - - -
def __addRow ( self, tbody, archImage ):
"""Add one row to the table.
[ (tbody is an xc.Element) and
(archImage is an archx.ArchImage) ->
tbody := tbody with a tr element added containing
the thumbnail and data from archImage ]
"""
#-- 1 --
# [ tbody := tbody with a new, empty tr element added
# tr := that tr element ]
tr = xc.Element ( tbody, "tr" )
Just to keep things uncomplicated, we'll delegate production of each of the three cells in the row to separate methods.
#-- 2 --
# [ tr := tr with a new td added containing the
# thumbnail for archImage ]
self.__addThumbnail ( tr, archImage )
#-- 3 --
# [ tr := tr with a new td added containing size data
# from archImage ]
self.__addSize ( tr, archImage )
#-- 4 --
# [ tr := tr with a new td added containing general
# data from archImage )
self.__addData ( tr, archImage )
This method generates the first column of the table, containing the image thumbnail; see Section 4.3, “XHTML for the form page”.
# - - - F o r m P h o t o S e t . _ _ a d d T h u m b n a i l
def __addThumbnail ( self, tr, archImage ):
"""Add a thumbnail image cell
[ (tr is an xc.Element) and
(archImage is an archx.ArchImage) ->
tr := tr with a new td added containing the
thumbnail for archImage ]
"""
#-- 1 --
# [ tr := tr with a new td element added
# td := that new td element ]
td = xc.Element ( tr, "td", valign="top", align="center" )
#-- 2 --
# [ td := td with a new img element added whose href
# is the thumbnail for archImage ]
href = "../thumb/%s.jpg" % archImage.original.catNo
xc.Element ( td, "img", src=href, alt="thumbnail" )
This method generates the second column of the table, displaying the image size. See Section 4.3, “XHTML for the form page”.
# - - - F o r m P h o t o S e t . _ _ a d d S i z e - - -
def __addSize ( self, tr, archImage ):
"""Generate the image size cell of the table.
[ (tr is an xc.Element) and
(archImage is an archx.ArchImage) ->
tr := tr with a new td added containing size data
from archImage ]
"""
#-- 1 --
# [ tr := tr with a new td element added
# td : = that new td element ]
td = xc.Element ( tr, "td", valign="top", align="right" )
The three lines in this cell are vertically stacked by
placing each inside a div element.
The frame area appears only for images that have a scan attribute that tells the dots per inch of
the scanner. If this attribute is missing, the original
is from a digital camera, and we don't display the frame
area at all.
To calculate the percentage frame area, we need to know the area in square millimeters of a full frame. Then we use the scanner precision, expressed in pixels per millimeter, plus the size of the image in pixels, to calculate the fraction of the frame. Here is the math:
The constant FULL_FRAME_MM2 gives the
area of a full frame in square millimeters: 24mm
× 36mm for a 35mm film frame.
Since 1 inch = 25.4mm, we can convert that value to square inches by dividing by the square of 25.4.
To convert square inches to pixels, we multiply by
the square of Pi, the dot pitch of the scanner in
dots per inch. This gives us Pf, the
number of pixels in a full frame at resolution
Pi, which is available as archImage.original.scan.
For an image with x pixels,
the image size as a percentage of the frame, then, is 100
× x / Pf.
#-- 2 --
# [ if archImage has a known dots/inch ->
# td := td with a div element added, containing the
# percentage of a full frame covered by archImage ]
if archImage.original.scan:
fullFramePixels = ( FULL_FRAME_MM2 / (MM_PER_IN**2) *
(archImage.original.scan)**2 )
thisFramePixels = archImage.wide * archImage.high
divPercent = xc.Element ( td, "div" )
framePercent = 100.0 * thisFramePixels / fullFramePixels
xc.Text ( divPercent, "%.2f%%" % framePercent )
The width and height are added in separate div elements; the latter is preceded by
“x”. It would be nice to have the
special Unicode character “×”
(code point 0x00d7), but so far I haven't been able to
figure out how to make it work.
#-- 3 --
# [ td := td with two divs added, the first containing
# the image width from archImage, the second
# containing 'x' and the image height ]
divW = xc.Element ( td, "div" )
xc.Text ( divW, str(archImage.wide) )
divH = xc.Element ( td, "div" )
xc.Text ( divH, "x" )
xc.Text ( divH, str(archImage.high) )
This method adds all the remaining catalog data to the
third column of the table row. First we add the cell's
td element.
# - - - F o r m P h o t o S e t . _ _ a d d D a t a - - -
def __addData ( self, tr, archImage ):
"""Add the catalog data cell to the table row.
[ (tr is an xc.Element) and
(archImage is an archx.ArchImage) ->
tr := tr with a new td added containing general
data from archImage )
"""
#-- 1 --
# [ tr := tr with a new td element added
# td := that new td element ]
td = xc.Element ( tr, "td", valign="top", align="left",
class_="cat-info" )
The elements inside this cell are stacked vertically
using div elements. The first div holds the catalog number, state, and
locality. The catalog number is additionally wrapped in
a span element of class cat-no.
#-- 2 --
# [ td := td with a new div element added
# majorDiv := that new div ]
majorDiv = xc.Element ( td, "div", class_="ident" )
#-- 3 --
# [ majorDiv := majorDiv with a new span element added
# containing archImage.original.catNo ]
catSpan = xc.Element ( majorDiv, "span", class_="cat-no" )
xc.Text ( catSpan, archImage.original.catNo )
Add the state code, always present. Add the locality data (and the colon that precedes it) only if there is locality data.
#-- 4 --
# [ majorDiv := majorDiv with archImage's state and
# locality added ]
xc.Text ( majorDiv, " " )
xc.Text ( majorDiv, archImage.original.state.upper() )
if archImage.original.loc:
xc.Text ( majorDiv, ": %s" % archImage.original.loc )
Eventually we'll add special divs for each element such
as pose, beh, and so on.
For now, just put up the note content.
#-- 5 --
# [ td := td with a new div element added containing
# archImage.original.note ]
if archImage.original.note:
noteDiv = xc.Element ( td, "div" )
xc.Text ( noteDiv, archImage.original.note )