Colin Champion

Summary: this page contains documentation and source for pix.js, comprising code to display photos from web pages, and for pixlib.js which is a library of functions to display an individual photo from within javascript or to display a thumbnail link from HTML.

pix.js uses a standard (XML) format for photo lists which specifies the attributes of each photo (filename stem, size, title, caption) and the sizes available fordisplay (their scale factors, filename suffices and types).

pix.js provides the javascript which does all the work for HTML pages displaying photos in the more recent manner of this website. See the Gran Canaria photo table for an example of the tabular display generated, and this cycling photo for an example of the display of an individual photo. There are navigation links which allow you to select smaller or larger images or to move to the next or previous one; the same functions are available from the arrow keys on a keyboard or from swipes and pinches at a touch screen.

          swipe left  =  ‘>’ icon  =  ‘→’ key  = next
swipe right=‘<’ icon =‘←’ key= prev
swipe down=‘↵’ icon =return key= back to notes/table
outwards pinch= ‘⊕’ icon =‘↑’ key= enlarge
inwards pinch= ‘⊖’ icon =‘↓’ key= reduce
    f key= enter full screen
    i key= photo info

You will notice from the URL in your browser that the photo table and the individual image are not two different pages but the same page with different parameters.

To use pix.js you need to create the photos (which is the hard part), enumerate them in a list, and then create a single 7-line page of simple HTML to invoke the javascript functions which give the different forms of display.

pix.js is freely shared under an MIT licence.

It is for you to decide whether this form of display suits you. The obvious alternatives are to keep your images on your local computer or to put them on the web on a photo sharing site (Flickr, Picassa,... ). Or you could write your own javascript or explicit HTML or perhaps find another module performing a similar function to pix.js. I don’t know of anything very similar, though simple viewer is not far off. There are also numerous tools for server-side image gallery management (see a wikipedia comparison page).

[Why are there so few similar products? Perhaps for this reason. There’s a continuum between tasks which are so small that you solve them for yourself every time you encounter them, and those so large you need an enterprise solution. The functions of pix.js fall just far enough along this continuum to justify having a reusable piece of software to perform them (which is why it exists at all), but until a few years ago may have fallen short of the threshold. Mobile devices, high-res screens, and an increasing willingness to rely on dynamic web pages may have changed the balance. And other people may have noticed the same and produced comparable products, but they may not have had time to establish themselves so that I could find them. But also partly because Web 2.0 has gobbled up Web 1.0, whose last denizen I claim to be.]

An advantage of using pix.js is that the photos are under your own control and you can integrate them with text content (eg. route notes) or with GPS tracks (and especially with routemaster). On the other hand you need to maintain a website of your own which requires a certain amount of knowledge, and there are tasks which a photo-sharing site will perform for you which you will have to perform for yourself. In particular a photo-sharing site can automatically generate thumbnails and different-sized versions of an image.

A single web page displays a set of pictures as a table or individual pictures from it. I create a separate directory for every collection of photos and put one picture page in it. The HTML for the web page is simplicity itself and mostly boilerplate.

<html><head>
<meta charset=utf-8> 
<script src="../../pixlib.js"></script>
<script src="../../pix.js"></script>
</head><body></body>
<script>pix("list.xml")</script>
</html>

Take this line by line.

 

<html><head>

Boilerplate. Obligatory.

 

<meta charset=utf-8> 

Boilerplate: necessary if you want to use non-Ascii characters from the larger Unicode set. I recommend you always include this line: it can’t do any harm.

 

<script src="../../pixlib.js"></script>
<script src="../../pix.js"></script>

Instruction to the browser to incorporate the pix.js and pixlib.js scripts: give a path to them from your page. If you want to use my own copies you will write

<script src="http://www.masterlyinactivity.com/pixlib.js"></script>
<script src="http://www.masterlyinactivity.com/pix.js"></script>

If you make your own copies, supply their relative URLs.

You can omit specifying pixlib.js, in which case you will get the version from masterlyinactivity.com.

 

</head><body></body>

Boilerplate. Obligatory.

 

<script>pix("list.xml")</script>

Invocation of the pix function: the argument is the name of the XML file defining the picture set. It is a uri, so it may be a relative or absolute path, e.g.

pix("http://www.meme.me/sillyphotos/cafepix.xml")

An early version of pix took a different argument, namely a link to your home page; I don’t know if this still works.

A subsequent version used a javascript format for picture lists, included as a <script src="..."></script> in the html header. This still works (and I still use if for our Sri Lanka pix page to keep it in shape) but is not accepted by routemaster and is now deprecated.

[I wrote listconv.c to convert js to XML photo lists.]

 

</html>

Boilerplate. Obligatory.

xml : aliases : images : dividing images into sections : the size list : the links list : sundry variables : example : image directories and duplicate image names : what is a srcset?

The photo list is more complicated. Look at an example of my own to see what the file contains.

The photo list is an XML specification of:

The contents of the photo list occur between photolist begin and end tags:

<photolist>
...contents...
</photolist>

Aliases can be used to avoid repetition of lengthy parameters. Note that all uris in the photo list are relative to the list itself (i.e. the XML file), not relative to some page which makes use of the photo list. This applies even to the links, whose sole purpose is to be included in the picture page. (In the old javascript format uris were relative to the picture page, the path from which to the parameter file was hidden.) To avoid confusion it is not a bad idea to keep the XML parameter file and the HTML picture page in the same directory.

XML is a format designed to be generated and read by computer programs; it is not ideal for human-generated input (sorry). In particular, formatting errors may be greeted by unintelligible or downright misleading error messages. XML is case-sensitive.

Information in an XML file is provided though a number of tags whose names are preceded by ‘<’ signs and closed by ‘>’ signs. Each tag is accompanied optionally by a set of attributes and optionally by a set of elements.

The attribute whose name is ‘suffix’ and whose value is ‘big’ will be specified as

suffix="big"

where the equals sign and the double quotes are part of the notation. The attribute is specified after the tag name but before the closing ‘>’ sign.

The elements governed by a tag comprise character strings and other tags. A tag does not need any elements, in which case it is called an ‘empty-element tag’. An empty-element tag has a solidus before its closing ‘>’. Most tags in photo lists are empty. An example is the size specification which takes the format

<size suffix="big" scale="700"/>

‘size’ is the tag name, ‘suffix’ and ‘scale’ the attribute names, and ‘big’ and ‘700’ the values.

If a tag has elements then it occurs in the form

<tag [attributes]>elements</tag>

i.e. there is no solidus preceding the ‘>’ which closes the first mention of the tag, but there is a second mention of the tag in which its name is preceded by a solidus.

There is an entry in the photo list for a page title. This entry has ‘title’ as its tag and the title itself as element, and has no attributes, so it is written

<title>Gran Canaria photos</title>

If you want to include non-Ascii characters you may either provide them as raw Unicode or give their numerical codes. The XML reference to a character whose hexadecimal code is abcd is ‘&#xabcd;’.

Aliases are used to abbreviate frequently occurring photo shapes and URL stems. The shape of an image is given as a character string in the format [w,h] where the components are width and height respectively; for instance an image might have attribute rawshape="[3264,2448]". To avoid repeating this string you may define an ‘rl’ alias for shapes as follows:

<alias name="rl" shape="[3264,2448]"/>

(The abbreviation ‘rl’ is supposed to stand for ‘raw landscape’.) You will then be able to specify the attribute as rawshape="rl".

You may also have a number of links to URLs with a common stem, and aliases can be used to abbreviate them and avoid repitition. The syntax is

<alias name="routes" 
       uri="http://www.masterlyinactivity.com/routemaster/?track=routes/grancanaria"/>

(Note that the final solidus is not part of the uri but indicates that the tag has no elements.)

Whenever an XML item has a uri as an attribute (e.g. href or gps) you may specify it in the form ‘u1,u2’ where the comma is a separator, there are no spaces, u2 is the uri relative to u1, and u1 is the name of a uri alias.

I can then specify the gps track in the photo section for our Artenara ride as

<section title="Valsequillo; the ridge path from Cruz de Tejeda to Artenara" gps="routes,artenara.tcx">

rather than as the equivalent but longer

<section title="Valsequillo; the ridge path from Cruz de Tejeda to Artenara" 
         gps="http://www.masterlyinactivity.com/routemaster/?track=routes/grancanaria/artenara.tcx">

An image is specified by the empty-element tag ‘img’ with a number of attributes.

The name is a short character string which you will use to identify a particular photo. The jpg file names will be based on the value of this field with suffixes appended for different versions and the ‘.jpg’ extension added. Eg.

name="swerve"

The title is a longer string which will be shown above the photo when it is displayed, or will appear in a box if you hover over its thumbnail. Eg.

title="Rocks near the Embalse de los Hornos"

The shape has the form [w,h] where the components are width and height respectively. So I could write ‘shape="[980,700]"’. However I seldom do this because the photos in a collection are nearly all of the same size except for orientation. So I set up two aliases ‘l’ and ‘p’ as the default landscape and portrait shapes and then write eg. ‘shape="l"’. The example page shows how this is done. Only if an image has an unusual shape do I specify its size explicitly.

A photo entry is given a single shape but the image will be available in different sizes – which version does the given shape apply to? The answer is that it applies to the first entry in the size list.

This ends the discussion of the obligatory fields. So a minimal photo entry may look like this:

<img name="swerve" title="Rocks near the Embalse de los Hornos" shape="p"/> 

The caption is an additional string which will be shown below the photo. Eg.

caption="This is harder than it looks!"

This field may take either of the values ‘"none"’ and ‘"|"’. ‘"none"’ means that the photo won’t in fact be displayed from your photo page (but may be used by other scripts looking at the same list, eg. routemaster).

A display of ‘"|"’ requests a weak horizontal divider before the thumbnail in a tabular display (which is not shown if the thumbnail occurs in the first column).

If you provide raw images you can specify a size for them individually which is not derived from the scale factor of the associated size (I do this myself because I use a 7x5 aspect ratio on the web which is not the same as the 4x3 imposed by our cameras.)

The shape of thumbnail images is usually inferred from the shape of main images or from a separately specified default size. Sometimes the inferred size will not be correct (most often because the image itself is an unusual shape), so the thumbnail shape needs to be specified explicitly. The format is an array of two numerical values, width and height. Eg.

thumbshape="[ 140 , 96 ]"

This field is not used except for photos for which a thumbshape has been specified. If a thumbnail has the same aspect ratio as the regular images then the regular images will be offered to the browser as hi-res alternatives to the thumbnail through the srcset attribute. If the aspect ratios differ then this cannot be done, but the user may provide a separate high-resolution thumbnail. If he does so he needs to specify its suffix and scale factor. This is done through the hithumb field in the photo list, whose value should be a string comprising the suffix and scale factor (in that order) separated by a comma, e.g. ‘hithumb="hith,2"’. The scale factor is the pixel size relative to the thumbnail (not relative to the first entry in the photo list). So if the photo shape is [1200,600] but the thumbshape is [140,196], and if there is additionally a hi-res thumbnail with suffix ‘"hith"’ and shape [420,588], then the entry in the photo list will be as shown in the maximal example.

Usually, rather than providing such a field for every image with a non-standard aspect ratio, you will supply a single hithumb value as a sundry variable.

These two values are used together. When you’re looking at an individual photo the [return] key or the ‘↵’ link will usually take you back to the tabular view. But if you’ve arrived at the photo from text notes, I’d like the [return] key to take you back to them at your current position. The retpage and retid field of a photo entry define the name of the HTML page and an anchor within it to comprise the link (with the ‘.html’ extension being automatically added to the page name). The return page should be in the same directory as the photo page and list file. If a retid is provided the URL will be ‘retpage.html#retid’. If not the URL will just be ‘retpage.html’.

You do not have to supply the same retpage and retid repeatedly. Once they have been supplied for a photo entry, they apply to subsequent entries until overridden.

This value specifies an image in which the named image files should be found. It is an alternative to the global imagedir described below.

This completes the description of fields defining a photo entry, so we can give a maximal example as a further illustration:

<img name="traceyrocks" title="Tracey heading for the Dog&#x2019;s Leap" 
     caption="looser than she would like" shape="p" display="|" 
     thumbshape="[140,196]" hithumb="hith,3" 
     retpage="grancanaria" retid="28" imagedir="images"/>

(Notice the Unicode escape sequence used to specify an apostrophe. I usually type escape sequences even though I’ve made sure the true Unicode character would be accepted.)

You may provide a list of images with no division into sections, but dividing them into sections is usually neater. You do this by means of the ‘section’ tag which has the images in the section as its elements.

The section tag has two attributes, title (which is mandatory) and gps which optionally supplies a link to a GPS track associated with the images in the section.

Remember to provide a closing ‘</section>’ after the images in the section but not to put a terminal solidus in the first mention of the section tag.

Here are some examples:

<section title="More rides from Valsequillo">
  <img .../>
  <img .../>
</section>

If we want to provide a GPS track the first line may instead read

<section title="More rides from Valsequillo"
         gps="http://www.masterlyinactivity.com/routemaster/?routes/grancanaria/artenara.tcx">

but it will look better if we make use of aliases and write

<section title="More rides from Valsequillo" gps="routes,artenara.tcx">

See the routemaster page for an explanation of how to provide a URL invoking routemaster on a particular track.

The size list comprises a single entry for every image size you support. You supply a sizes tag which has no attributes but a number of elements, each with the size tag and no elements of its own but attributes defining the size. Hence you write

<sizes>
  <size .../>
  <size .../>
</sizes>

The attributes of the size tag are as follows.

This is a character string which will be appended to a photo name to construct the jpg filename. It may be empty. A non-empty example is

suffix="@big"

If we are displaying an image whose name (in the photo list) is swerve, and for which the suffix corresponding to the chosen size is ‘big’, then the filename will be swervebig.jpg. This may be modified by a directory path.

My practice to date has been to use empty suffix for an arbitrary size (in fact a good jpg size 15 years ago); I think it would be better to reserve it for the raw size, which is what I do in this documentation.

This is a numerical value giving the relative size of images with the given suffix. The only circumstance in which it may be omitted is for the suffix of type ‘thumb’ if a separate thumbshape is being provided.

This is a character string specifying a type corresponding to the image size. For normal display images it should be omitted. A single entry in the size list should have type ‘thumb’; this is the entry understood as corresponding to thumbnails. Optionally a further entry may have type ‘raw’ understood as corresponding to the raw image. Raw images are treated the same as regular images with the following exceptions:

The size in px for the heading text for photos (the caption is 4/5 the header size). Eg.

fontsize="12"

The links are provided as single items in empty-element link tags. These entries are put into the links line at the top and bottom of a picture table. Two links will be added automatically: if any pictures have a retpage, then it will be given a link as ‘notes’, and if full screen is available a link to invoke it will be provide as ‘enter full screen’ with the ‘f’-key as an equivalent.

The attributes of a link are as follows.

This is a the textual name to give to a link, eg. ‘home’.

This is the relative path or absolute path to the linked page, eg. ‘../../index.html‘.

So a simple link item will be

<link name="home" href="../../index.html"/>

You may provide a title for your pix.html page either in the header of that page or in the xml photo list. The latter is recommended because it makes the title available to other software which uses the photo list (e.g. routemaster). It belongs at the top and is formatted exactly the same as an HTML title:

<title>Gran Canaria cycling photos</title>

You may provide a shortcut icon for your pix.html page either in the header of that page or in the xml photo list. The sole advantage of the latter is that it allows you to specify the page content entirely from the XML rather than sharing duties between that and the HTML. The format is

<icon href="..."/>

The photo jpgs do not need to be stored in the same directory as the pix.html picture page. It’s convenient to put them in a subdirectory but they can be anywhere. The imagedir variable should be used to define the path from the location of pix.html to the jpgs, eg.

<imagedir href="images"/>

This is an alternative to specifying imagedirs for individual images in the photo list as described above. See the discussion on image directories below for observations on how to use these parameters.

This variable should be a string holding the path to the picture page from the list file; for me it’s always "pix.html".

This variable is not used by pix.js: the picture page does not need to be told its own location. But if other pages or scripts want to use the same photo list they’ll want to be able to link to a tabular display of the photos so they need the name of this page.

<pixpage href="pix.html"/>

My own practice is for thumbnails to be scaled-down versions of the main image, eg. one fifth the size of the largest. This means that some are landscape and some are portrait, which makes a tabular view rather ragged. Tables will look neater if the thumbnails all have the same shape (photo sharing sites usually make them square). If you want to do this the thumbnail shapes will not be inferrable from the shapes of display images in the photo list so you need to specify them separately, but it is sufficient to supply a single value rather than a field for each image, eg.

<thumbshape shape="[ 100 , 100 ]"/>

If there are photos whose thumbnails have a different aspect ratio than the regular images, you are recommended to provide a hi-res thumbnail alternative for them – that is, an image of the same aspect ratio as the thumbnail but a larger pixel size (a factor of 3 is about right). You may then specify a suffix and scale factor separately for every image in question through the hithumb field, but if you will be entering the same value each time, it is more convenient to specify the value once through the hithumb variable, eg.

<hithumb suffix="hith" scale="3"/>

The precise algorithm used by pix.js is this:


<title>Gran Canaria mountain biking photos</title>
<icon href="../../favicon.gif"/>
<alias name="l" shape="[980,700]"/>
<alias name="p" shape="[700,980]"/>
<alias name="rl" shape="[3072,2304]"/>
<alias name="rp" shape="[2304,3072]"/>
<alias name="routes" uri="http://www.masterlyinactivity.com/routemaster/?track=routes/grancanaria"/>

<section title="Valsequillo; the ridge path from Cruz de Tejeda to Artenara"
         gps="routes,artenara.tcx">
  <img name="swerve" title="Rocks near the Embalse de los Hornos" shape="p"
       rawshape="rp" retpage="grancanaria" retid="17"/>
  <img name="ridgepath" title="Ridge path" shape="p" rawshape="rp" display="|"
       retid="19"/>
  <img name="tracey" title="Tracey on the ridge, Roque Nublo behind" shape="p"
       rawshape="rp"/>
  <img name="precipice" title="Colin on the ridge" shape="p" rawshape="rp"
       caption="keeping well to the right"/>
  <img name="artenara" title="Colin on the ridge" shape="p" rawshape="rp"/>
</section>
<section title="More rides from Valsequillo and Chira">
  <img name="nublo" title="Roque Nublo" shape="l" rawshape="rl"/>
  <img name="teide" title="Teide" shape="l"
       caption="Protruding from a sea of cloud" retid="21"/>
  <img name="teror" title="Track descending to Teror" shape="p" rawshape="rp"
       display="|" retid="22"/>
  <img name="greentrack" title="Lush valley" shape="l" rawshape="rl"/>
</section>
<section title="Camino Real" gps="routes,real.tcx">
  <img name="lava" title="Tracey on the lava on the Camino de Santiago"
       shape="l" rawshape="rl" retid="24"/>
  <img name="photo" title="Colin descending the zigzags" shape="l"
       rawshape="rl"/>
  <img name="zigzag" title="Zigzags" shape="p" rawshape="rp" retid="24"/>
</section>
<section title="Degollada de la Manzanilla and other rides around Chira"
         gps="routes,manz.tcx">
  <img name="manz" title="Descending from the Degollada de la Manzanilla"
       shape="p" rawshape="rp" retid="27"/>
  <img name="rocks" title="(Tracey did this too)" shape="p" rawshape="rp"/>
  <img name="traceyrocks" title="Tracey heading for the Dog&#x2019;s Leap"
       shape="p" rawshape="rp" display="|" retid="28"/>
  <img name="narrow" title="Pipe track" shape="p" rawshape="rp"/>
  <img name="dinner" title="Dinner at Chira" shape="l" rawshape="rl"/>
</section>

<sizes>
  <size suffix="big" scale="700"/>
  <size suffix="huge" scale="1000"/>
  <size suffix="med" scale="500"/>
  <size suffix="" scale="350"/>
  <size suffix="raw" scale="0" type="raw"/>
  <size suffix="thumb" scale="140" type="thumb"/>
</sizes>

<link     name="home" href="../../index.html"/>
<link     name="GPS tracks" href="routes,index.tcx"/>
<imagedir href="images"/>
<pixpage  href="pix.html"/>
</photolist>

My original plan was that all images should lie in the same directory as the pix.html page, but this is obviously clumsy. I added imagedir to obviate the clumsiness but required that all images should lie in the same directory. However there are circumstances in which this may be unsatisfactory; for instance, you may want to construct a composite list comprising a selection of photos from different lists, each with its own imagedir. So I allowed the image directories to be specified from the photo list instead.

Precedence between the imagedirs is decided as follows.

Firstly, an imagedir in the photo list overrides any global imagedir.

Secondly, if a photo in the list does not have its own imagedir, and if a global imagedir has been specified, then the global imagedir will be used for that image.

Thirdly, if a photo does not have its own imagedir and there is no global imagedir, but a photo earlier in the list does have its own imagedir, then the most recent such imagedir will be used.

This make it easy to adopt either of the commonest patterns of use.

If most of the images come from the same directory, but one or two are drawn from elsewhere, then specify a global imagedir for the location containing most of the images and item-specific imagedirs for the exceptions.

If you are combining several lists (each with its own associated directory), specify imagedirs in the new list, only needing to do so for the first in each sequence drawn from the same directory.

If you combine photos from several directories you may find you have reused the same name between them. This is not inherently problematic because the photos whose names clash must lie in different directories. The pix.js software won’t get confused.

However if you want to access these images externally (through the thumb() or findimage() functions which access an image through its name) you may need to take special measures.

Both of these functions return the first occurrence of the image name in the list unless instructed to do otherwise. The instruction to do otherwise is issued through an optional additional serialno parameter to both functions. If serialno is 0 you get the first occurrence of the name; if it is 1 you get the second; and so forth. [No compromises are made for users who can’t count from 0.]

Since srcsets are relatively new a few words of explanation may be useful.

When HTML started to be used it was common for page authors to specify dimensions in pixels knowing that there were 96 (or occasionally 72) to the inch. They could, in most cases, have specified dimensions in other units instead (eg .inches) but if they’d done so they’d have had no control of the pixel alignment of the result. They avoided this because a pixel-wide line would look blurry on the screens of the time if not exactly aligned with a physical pixel.

Similar considerations applied to jpgs. If a jpg had an intrinsic size of 600x400 pixels then it was best to display it at that size rather than, say, as 6"x4". Respecting the true pixel dimensions ensured that the image was shown at the best possible quality and required minimal processing from the browser. If the browser was required to resize an image it had the choice of algorithms which were computationally expensive but imposed only a slight degradation in quality, or simple algorithms which introduced conspicuous artefacts. Web authors avoided the dilemma whenever they could.

When LED screens came in it became possible (and for mobile devices necessary) to make the physical pixels much smaller. Browsers then had the choice of interpreting pixel dimensions literally, which would make almost everything on the screen 3 times smaller than the author had intended, or of reinterpreting pixels as implicit sizes of about a hundredth of an inch. The latter decision imposed itself (although the sizes are actually defined in terms of angles subtended to the eye rather than as true physical dimensions).

It follows that a 600x400 image continues to be displayed at the size the author intended but that it does not exploit the screen’s high resolution capability. For existing pages that could be hardly be avoided since taking advantage of hi-res screens would require an image whose intrinsic size was more like 1800x1200, and the author hadn’t provided one.

For new pages, though, authors can make allowance for hi-res displays by providing alternative images through the srcset attribute.

In pix.js it is very natural to do this since I allow images to be supplied at a range of sizes for other reasons. I offer the large ones as hi-res srcset alternatives to the small ones; and if you want hi-res alternatives to the large ones, you can always provide still larger ones (but don’t go mad – there are only so many receptors in the eye).

This works easily for regular images, and regular images can be offered as hi-res alternatives to thumbnails if they have the same aspect ratio; but if the aspect ratios differ I have to take extra measures through the hithumb values.

The effort is rewarded though. My laptop has a ‘retina’ screen and at first I had no intention of supplying hi-res thumbnails, but at a certain point they crept in through a bug in my code. The difference in sharpness was striking to the eye so I made sure they stayed in and were properly supported for non-standard aspect ratios.

xml : aliases : images : dividing images into sections : the size list : the links list : sundry variables : example : image directories and duplicate image names : what is a srcset?

You may want to provide thumbnail links to images from a separate HTML file. For this purpose you are encouraged to use the thumb() function in pixlib.js (whose other uses are described in below). Our Gran Canaria notes are an example of a page using it.

In the HTML page you write

<script>thumb("swerve")></script>

This expands as the full HTML

<a href=...><img src=... width=... height=... title=... srcset=... class=pix></a>

where the triple dots refer to values computed by looking at the entry for swerve in the photo list.

To make use of this function you include the following two lines in your HTML header (in either order):

<script src=list.js></script>
<script src="../../pixlib.js"></script>

The first instructs the browser to incorporate your file list: provide the appropriate path. The second instructs it to incorporate the pixlib.js javascript.

As before you are free either to use my definitive version of the script by writing

<script src="http://www.masterlyinactivity.com/pixlib.js"></script>

or to make your own copy and supply its relative URL.

You will also want to define the img.pix style appropriately, eg. in the line

<style>img.pix{border:1px solid}</style>

But usually you will put the style declaration amongst others either in the header or in a style sheet.

If there are duplicate image names in your list and you want a thumbnail for the second with a given name, you should write (eg.)

<script>thumb("swerve",1)></script>

An old javascript alternative thumbs.js is now deprecated.

getphotolist : setthumbshape : gendisplay : uncaption : reluri : keyboard events : image utilities : full screen utilities

pixlib.js is a library of javascript functions for displaying individual images exactly as by pix.js (which of course uses it). The interface is defined by the following functions. Note that link colours need to be set by the calling page: javascript can’t set pseudo-class attributes the way it can set genuine classes.

Break out properties from an XML photolist.

  props = getphotolist(xmldoc,xmlfile) ;

where

You obtain the xmldoc by a sequence such as the following:

  var xmldoc , xhttp = new XMLHttpRequest() , props ;
  var parser = new DOMParser() ;
  xhttp.onreadystatechange = function() 
  { if(xhttp.readyState==4) 
    { if(xhttp.status==200)
      { xmldoc = parser.parseFromString(xhttp.responseText,"application/xml") ;
        props = getphotolist(xmldoc,xmlfl) ; 
        ... do whatever you like with props... 
      }
      else alert("Unable to read "+xmlfl+": error code "+xhttp.status) ;
    }
  }
  xhttp.open("GET",xmlfl,true) ;
  xhttp.send() ;

xmlfile is needed as an argument to getphotolist so that uris in the photo list (which as specified are relative to the location of the XML) can be modified so as to be relative to the host page.

The return value of getphotolist is an object whose fields are list, the photo list; sizes, the size list; links, the links list; pixpage, the picture page; and maxthumb, the maximum thumb size. Some of the XML parameters are not returned but simply used to construct fields in the photolist. The format of list items is opaque; they should be used as arguments to the other pixlib functions. But you should know that each list item contains a filename field giving a uri for the photo. These filenames (and the pixpage) will be returned as uris relative to the host page, having been adjusted from the values in the XML file which are relative to that file itself.

  maxthumb = setthumbshape(list,sizes,thumbshape,imagedir,hithumb) ;

where the arguments are the values supplied through list.js (the last three are optional). The return value is a 2-long array [maxw,maxh] containing the maximum width and height of thumbnails.

setthumbshape fills in missing fields from the list and sizes arrays if these were loaded as Javascript (rather than XML). You must call it before gendisplay in order to get correct behaviour. Do not use it for parameters obtained using getphotolist.

   gendisplay(element,item,sizes,llink,blink,rlink,fromstring,fetchitem)  ;

is the call to generate an image display.

gendisplay sets up event handlers for image loads. When you regain control of the display (eg. after a return-to-parent) it cannot be excluded that these event handlers will be subsequently triggered, or at least that variables they access will remain live. To explicitly relinquish them for garbage collection call gendisplay with no arguments, ie.

  gendisplay() ;

This will mark all pixlib’s static variables as garbage.

If the user hovers over a caption, a text box will be brought up showing the caption in a larger font; this is useful if the caption is too long for a single line, in which case the normal display truncates it. If the programmer wants to force this box to disappear, he may call

  uncaption() ;

which is useful if, for instance, he is changing elements in the document.

  u = reluri(u1,u2) ;

returns the uri u relative to the host page of some file u2 when u1 is the uri of some other page relative to the host and u2 is the uri of the file relative to u1. E.g.

  u = reluri("home/list.xml","images/img1.jpg") ;

returns u as ‘home/images/img1.jpg’.

The return value is null or undefined whenever u2 is null or undefined. This is usually what you want but you need to be aware of an exception. If you build up a uri as u1+null+u3 then you expect the result to be the same as u1+u3, but if you compute it as

  u = reluri(reluri(u1,null),u3) ;

you will instead get just u3.

Links in the navigation table allow enlargement, reduction and navigation to other images or to the parent display. Other keyboard inputs may have the same effects: in particular, it is expected that the arrow keys and the return key will be equivalent to the navigation icons and that swipes and pinches will have their natural effects. pixlib does not set up event handlers for them; it expects you to do so and to request the corresponding action through the pixlib interface. You do this through the following three calls:

All three functions have no arguments.

You are also expected to detect events equivalent to the ‘<’, ‘↵’, and ‘>’ icons, but when these occur you are expected to respond to them yourself, i.e. to do whatever your llink, blink and rlink hrefs do.

The functions described above are all you need to use to produce an image display. pixlib.js also contains some utilities you might want to use yourself to handle images in the list file, and a few simple utilities for accessing full-screen functions.

findimage() has an optional additional parameter. If there is more than one image with a given name in a list, and you don’t want the first one, you should supply the serial number of the one you want as the extra parameter. So if you want the second image under the name swerve in the list you should write

  findimage(list,"swerve",1) ;

if you want the third you should write

  findimage(list,"swerve",2) ;

and so forth. See the section on imagedirs for some discussion.

getphotolist : setthumbshape : gendisplay : uncaption : reluri : keyboard events : image utilities : full screen utilities

pix.js generates images as you can see them on our web site. But it does one or two things you may not be aware of which it may be worth listing. Some of these (unusually for me) take advantage of recent additions to HTML.

Before I wrote pix.js I had got into the habit – starting around 2001 – or displaying individual photos on pages with navigation icons at the top left. enlarge and reduce icons toggled between two sizes whose ratios and suffices were hard-coded. The page background was a beigey colour.

Around 2013 I switched to a black background and added an optional third size. Either earlier or later I added the option of providing just a single image size. I broke out the image generation code as a separate javascript file but it was written only for my own use and was full of hard-coded assumptions.

In February 2016 I made it more generic, documented it, and made it generally available.

I made a major revision at the end of March. The (rather trifling) functional changes were to make allowance for raw images and to centre images vertically on the screen as well as horizontally.

The non-functional changes were more onerous. I switched from using a table to define the screen layout to using divs. This gives me more control (and may protect me from some of w3c’s strictures) but makes the response jerkier when the user resizes the window.

At the same time I formalised and documented the interface at a lower level, that of pixlib.js. This cleans things up and may be useful to someone sometime. I intend to use pixlib.js for the image display in routemaster.

I debugged the swipes and pinches on Tracey’s iPad.

Added the links list; deprecated the argument to pix(); added genimage(); added srcset to thumbnails; added hithumb; deprecated thumbs.js.

Bugfix to thumbnail srcsets. Added licence.

Changed the URL parms to make them google-friendlier. Added pixinfodiv to pixlib.js and i-button to pix.js. Another bugfix to thumbnail srcsets.

Added imagedir as a field in photo lists; added serialno parameter to disambiguate name clashes. Timeout rather than mouseout terminates a captionbox (I couldn’t get the mouseout to fire reliably).

Added the XML input format and the history.pushState() method of moving from one photo to another.

I can imagine adding an icon size and breaking out the tabular display to pixlib but don’t have any inclination to do so.

• uninfodiv     • retitle     • pixresize     • getncol     • linkp     • piclink     • redisplay     • display     • navigate     • info     • tabulate     • optionparse     • pix     • function     • render     • simulate     • startswipe     • midswipe     • endswipe     • tabinfodiv

// www.masterlyinactivity.com/software/pix.html
/* The MIT License

   Copyright (c) 2016, by Colin Champion <colin.champion@masterlyinactivity.com>

   Permission is hereby granted, free of charge, to any person obtaining
   a copy of this software and associated documentation files (the
   "Software"), to deal in the Software without restriction, including
   without limitation the rights to use, copy, modify, merge, publish,
   distribute, sublicense, and/or sell copies of the Software, and to
   permit persons to whom the Software is furnished to do so, subject to
   the following conditions:

   The above copyright notice and this permission notice shall be
   included in all copies or substantial portions of the Software.

   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
   EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
   MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
   NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
   BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
   ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
   CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
   SOFTWARE.
*/
var list,sizes,thumbshape=[],imagedir=null,hithumb,pixpage,maxthumb=null ;
document.write( '<style>a:link{color:#66aaaa}' + 
                'a:visited{color:#cc3388}a:active{color:#404040}</style>' ) ; 
if((typeof pixlib)=="undefined") document.write
  ('<scri'+'pt src="http://www.masterlyinactivity.com/pixlib.js"></scri'+'pt>');
var thispage,nload=0,here,lind,rind,blink,pagetitle,retpage ;
var nitem=[],infodiv=null ;
var llink,rlink,ulink,dlink,prevind,fromnotes ;

function uninfodiv()
{ if(infodiv!=null) 
  { document.getElementsByTagName("body")[0].removeChild(infodiv) ;
    infodiv = null ; 
  }
}
function retitle(newtitle)
{ var titlestr,title = document.getElementsByTagName("title")[0] ; 
  while(title.childNodes.length>1) title.removeChild(title.firstChild) ;
  titlestr = document.createTextNode(newtitle)
  if(title.childNodes.length==0) title.appendChild(titlestr) ;
  else title.replaceChild(titlestr,title.childNodes[0]) ;
}
/* -------------------------------------------------------------------------- */

// pixresize responds to a window resize as follows:
// o. for a table view, if the number of columns can change, change it;
// o. for an image view, if the new size permits a larger or smaller image, 
//    enlarge or reduce;
// o. or if the window is expanding or contracting but hasn't changed size 
//    enough to change the desired image, anticipatively preload a larger or
//    smaller one.

function pixresize()
{ if(here.name!=null) resize()
  else if(queryfullscreen()!=here.full||here.ncol!=(k=getncol())) tabulate() ;
}
/* ----- getncol finds the number of table columns which fit the screen ----- */

function getncol()
{ var maxcol,ncol,k,ind ; // 17 pix for scrollbar, 19 pix for margin, border...
  maxcol = Math.floor((window.innerWidth-17)/(maxthumb[0]+19)) ; 
  if(maxcol<1) maxcol = 1 ;  
  for(ncol=ind=0;ind<nitem.length;ind++) 
  { k = Math.ceil(nitem[ind]/Math.ceil(nitem[ind]/maxcol)) ;
    if(k>ncol) ncol = k ; 
  }
  return ncol ;
}
/* -------------------------------------------------------------------------- */
/*page*/
/* -------------------- create links line of the table page ----------------- */

function linkp()
{ var p,a,span,astyle,i,flag=0 ; 
  astyle = 'font-family:helvetica;font-weight:normal;text-decoration:none;' ;
  
  p = document.createElement("p") ; 
  p.setAttribute('style',"text-align:center;font-size:100%;margin:6px;"+
                         "font-family:helvetica;color:silver") ; 

  if(retpage!=null)
  { a = document.createElement('a') ; 
    a.setAttribute('href',list[retpage].retpage+'.html') ; 
    a.setAttribute('style',astyle) ; 
    a.appendChild(document.createTextNode('notes')) ; 
    p.appendChild(a) ; 
    flag = 1 ;
  }

  if(links!=null) for(i=0;i<links.length;flag=1,i++)
  { if(flag) p.appendChild(document.createTextNode(' : ')) ; 
    a = document.createElement('a') ; 
    a.setAttribute('href',links[i].href) ; 
    a.setAttribute('style',astyle) ; 
    a.appendChild(document.createTextNode(links[i].name)) ; 
    p.appendChild(a) ; 
  }

  if(queryfullscreen()==0&&querycanfullscreen())
  { if(flag) p.appendChild(document.createTextNode(' : ')) ; 
    a = document.createElement('a') ; 
    a.setAttribute('style',astyle) ; 
    a.setAttribute('href','javascript:{}') ; 
    a.setAttribute('onclick','enterfullscreen()') ; 
    a.appendChild(document.createTextNode('full screen')) ; 
    p.appendChild(a) ; 
    span = document.createElement('span') ;
    span.setAttribute('style',"color:gray") ; 
    span.appendChild(document.createTextNode(' [f key] ')) ;
    p.appendChild(span) ;
  }

  p.appendChild(document.createTextNode(' : ')) ; 
  a = document.createElement('a') ; 
  a.setAttribute('style',astyle) ; 
  a.setAttribute('href','javascript:info()') ; 
  a.appendChild(document.createTextNode('info')) ; 
  p.appendChild(a) ; 
  span = document.createElement('span') ;
  span.setAttribute('style',"color:gray") ; 
  span.appendChild(document.createTextNode(' [i key] ')) ;
  p.appendChild(span) ;

  return p ;
}
/* -------------------------------------------------------------------------- */

function piclink(ind)
{ var s = 'javascript:redisplay("' + list[ind].name + '"' ;
  if(list[ind].serialno>0) return s + ',' + list[ind].serialno + ')' ;
  else return s + ')' ;
}
function redisplay(name,serialno)
{ var ind = findimage(list,name,serialno) ;
  var thisuri = thispage + '?image=' + name ;
  if(serialno>0) thisuri += '&serialno=' + serialno ;
  history.pushState(null,'',thisuri) ; 
  display(ind) ;
}
/* -------------------------------------------------------------------------- */

function display(ind)
{ var titlestr,body,title,i,k,fetchitem,s ; 
  var name=list[ind].name,serialno=list[ind].serialno ;
  uninfodiv() ; 
  body = document.getElementsByTagName("body")[0] ; 

  if(name!=here.name||serialno!=here.serialno) retitle(list[ind].title) ; 

  here = { name:name , 
           serialno:serialno , 
           ind:ind , 
           ncol:0 , 
           full:queryfullscreen() } ;

  // navigation links: <
  for(lind=ind-1;
      lind>=0&&(list[lind].name==undefined||list[lind].display=='none');
      lind--) ; 
  if(lind<0) llink = null ; else llink = piclink(lind) ;

  // navigation links: >
  for(rind=ind+1;
      rind<list.length &&
        (list[rind].name==undefined||list[rind].display=='none');
      rind++) ; 
  if(rind==list.length) rlink = null ; else rlink = piclink(rind) ;

  // navigation links: return
  if(fromnotes==0) { blink = 'javascript:tabulate()' ; s = "table" ; }
  else
  { blink = retlink(list,ind) ;
    if(blink==null) alert('no return page for '+
                          list[ind].name+(serialno>0?('('+serialno+')'):'')) ; 
    s = "notes" ;
  }

  // decide which image if any to prefetch 
  if(prevind!=null&&prevind>ind&&lind>=0) k = lind ; 
  else if((prevind==null||prevind<=ind)&&rind<list.length) k = rind ; 
  else k = null ;
  if(k==null) fetchitem = null ; else fetchitem = list[k] ;

  gendisplay(body,list[ind],sizes,llink,blink,rlink,s,fetchitem) ; 
}
/* ------------------------ navigate using the arrow keys ------------------- */

function navigate(e) 
{ var d ; 
  if(infodiv!=null) { uninfodiv() ; if(e.keyCode==73) return ; }

  if(e.keyCode==37&&lind>=0)          // left arrow key
  { e.preventDefault() ; 
    prevind = lind+1 ; 
    redisplay(list[lind].name,list[lind].serialno) ; 
  }
  else if(e.keyCode==39&&rind<list.length) // right arrow key
  { e.preventDefault() ; 
    prevind = rind-1 ; 
    redisplay(list[rind].name,list[rind].serialno) ; 
  }
  else if(e.keyCode==13&&here.ncol==0)  // return
  { e.preventDefault() ; 
    if(fromnotes==0) 
    { retitle(pagetitle) ; history.pushState(null,'',thispage) ; tabulate() ; }
    else location.href = blink ; 
  } 
  else if(e.keyCode==40&&here.ncol==0) { e.preventDefault() ; reduce() ; } 
  else if(e.keyCode==38&&here.ncol==0) { e.preventDefault() ; enlarge() ; } 
  else if(e.keyCode==70&&queryfullscreen()==0) // 'f' (full screen)
  { e.preventDefault() ; enterfullscreen() ; }
  else if(e.keyCode==73) info() ; 
}
function info()
{ if(infodiv!=null) { uninfodiv() ; return ; } 
  if(here.ncol>0)infodiv = tabinfodiv(list) ; 
  else infodiv = pixinfodiv(list,here.ind,sizes) ; 
  infodiv.setAttribute('style','position:fixed;bottom:20;right:20;'+
   'background:white;padding:8;opacity:0.8') ;
  document.getElementsByTagName("body")[0].appendChild(infodiv) ; 
}
/* -------------------------------------------------------------------------- */
/*page*/
/* -------------------------------------------------------------------------- */

function tabulate()
{ var ind,ncol,p,body,table,tr,td,a,img,trflag,k,s,opts,thind=thumbind(sizes) ; 
  var prefetch,nimg,padflag,k,num ; 
  uninfodiv() ; 
  gendisplay() ; 
  rind = list.length ;
  lind = -1 ;

  body = document.getElementsByTagName("body")[0] ; 
  while(body.firstChild) body.removeChild(body.firstChild) ;

  ncol = getncol() ;
  here = { name:null , 
           serialno:null , 
           ind:null , 
           ncol:ncol , 
           full:queryfullscreen() } ;
  // prefetch will be performed when nimg thumbs have been loaded
  for(nimg=ind=0;ind<list.length;ind++) 
    if(list[ind].name!=undefined&&list[ind].display!='none') nimg += 1 ; 

  p = document.createElement("p") ; 
  p.setAttribute('style',"text-align:center;font-size:140%;margin:2px 0 6px;"+
                 "font-family:helvetica;color:silver;") ; 
  p.appendChild(document.createTextNode(pagetitle)) ; 
  body.appendChild(p) ; 

  body.appendChild(linkp()) ; 

  table = document.createElement('table') ;
  table.setAttribute('cellspacing','0') ; 
  table.setAttribute('cellpadding','0') ; 
  table.setAttribute('align','center') ; 
  for(s='',ind=0;s==''&&ind<list.length;ind++) 
    if(list[ind].name==undefined) 
      s = "border-bottom:1px solid #444;padding-bottom:4px;" ; 
  table.setAttribute('style',s+'margin:0px auto') ; 

  for(prefetch=null,nload=trflag=colno=ind=0;ind<list.length;ind++)
    if(list[ind].display!='none') 
  { if(0==colno%ncol||list[ind].name==undefined) 
    { if(trflag>0) table.appendChild(tr) ; 
      tr = document.createElement('tr') ; 
      trflag = 0 ; 
    } 
    td = document.createElement('td') ;

    if(list[ind].name==undefined)
    { td.setAttribute("style","border-top:1px solid #444") ; 
      td.setAttribute('align','left') ;  
      td.setAttribute('colspan',ncol) ;  
      p = document.createElement("p") ; 
      p.setAttribute("style","font-size:110%;padding-top:6px;"+
                             "font-family:helvetica;color:silver") ; 
      if(list[ind].gps==undefined||list[ind].gps==null)
        p.appendChild(document.createTextNode(list[ind].title)) ; 
      else
      { p.appendChild(document.createTextNode(list[ind].title+' : ')) ;
        a = document.createElement('a') ; 
        a.setAttribute('href',list[ind].gps) ; 
        a.setAttribute('style','font-size:90%;text-decoration:none') ; 
        a.appendChild(document.createTextNode('[GPS track]')) ;
       p.appendChild(a) ;
      }
      td.appendChild(p) ; 
      tr.appendChild(td) ; 
      table.appendChild(tr) ; 
      tr = document.createElement('tr') ; 
      colno = trflag = 0 ; 
      continue ; 
    } 

    lind = ind ;
    llink = piclink(lind) ;
    if(rind==list.length) { rind = ind ; rlink = piclink(rind) ; }
    // extra padding at the bottom before a title row
    if(0==colno%ncol) 
    { padflag = 4 ; 
      for(num=0,k=ind;k<list.length&&num<ncol&&list[k].name!=undefined;k++) 
        if(list[k].display!='none') num += 1 ;
      if(k<list.length&&num<=ncol) 
      { padflag = 8 ; if(ncol>5) padflag += 2*(ncol-5) ; }
    } 
    td.setAttribute('align','center') ;  
    if(list[ind].display=='|'&&colno%ncol>0)
      td.setAttribute('style','border-left:1px solid #444') ;
    a = document.createElement('a') ; 
    a.setAttribute('href',piclink(ind)) ;
    a.setAttribute("class","box") ; 
    a.setAttribute("title",list[ind].title) ; 
    img = genimage(list[ind],sizes,thind,function() 
            { nload += 1 ; 
              if(nload==nimg&&prefetch!=null) genimage(list[prefetch],sizes) ; 
            } ) ;
    img.setAttribute('border',1) ; 

    // vertical bar: margin is t-r-b-l or t-lr-b
    if(colno%ncol>0&&list[ind].display!='|') 
      img.setAttribute('style',"padding:4px;margin:4px 4px "+padflag+"px 5px") ;
    else img.setAttribute('style',"padding:4px;margin:4px 4px "+padflag+"px") ; 

    a.appendChild(img) ; 
    td.appendChild(a) ; 
    tr.appendChild(td) ; 
    trflag = 1 ; 

    if(prefetch==null) prefetch = ind ;
    colno += 1 ;
  }
  table.appendChild(tr) ;
  body.appendChild(table) ; 
  body.appendChild(linkp()) ; 
}
/* -------------------------------------------------------------------------- */

function optionparse(s)
{ var ind,query=null,serialno=0,mode='' ;
  if((ind=s.indexOf('image='))>=0)
  { ind += 6 ; 
    for(end=ind+1;end<s.length&&s.charAt(end)!='&';end++) ; 
    query = s.substring(ind,end) ; 
  }
  if((ind=s.indexOf('serialno='))>=0)
  { ind += 9 ; 
    for(end=ind+1;end<s.length&&s.charAt(end)!='&';end++) ; 
    serialno = parseInt(s.substring(ind,end)) ; 
  }
  if((ind=s.indexOf('mode='))>=0)
  { ind += 5 ; 
    for(end=ind+1;end<s.length&&s.charAt(end)!='&';end++) ; 
    mode = s.substring(ind,end) ; 
  }
  return { name:query , serialno:serialno , mode:mode } ;
}
/* -------------------------------------------------------------------------- */
/*page*/
/* -------------------------------------------------------------------------- */

var parser ;

function pix(xmlfl)
{ var body = document.getElementsByTagName("body")[0] ; 
  body.setAttribute('style','background:black;margin:0') ;
  if(xmlfl==undefined||xmlfl.substring(xmlfl.length-4).toLowerCase()!='.xml') 
  { render(null) ; return ; }

  var xmldoc , xhttp = new XMLHttpRequest() , props ;
  parser = new DOMParser() ;
  xhttp.onreadystatechange = function() 
  { if(xhttp.readyState==4) 
    { if(xhttp.status==200)
      { xmldoc = parser.parseFromString(xhttp.responseText,"application/xml") ;
        props = getphotolist(xmldoc,xmlfl) ; 
        list = props.list ;
        sizes = props.sizes ;
        links = props.links ;
        pixpage = props.pixpage ;
        maxthumb = props.maxthumb ;
        render(props.title,props.icon) ; 
      }
      else alert("Unable to read "+xmlfl+": error code "+xhttp.status) ;
    }
  }
  xhttp.open("GET",xmlfl,true) ;
  xhttp.send() ;
}
/* -------------------------------------------------------------------------- */

function render(title,ic)
{ var ind,query,k,r,prev,h,head=document.getElementsByTagName('head')[0] ;
  thispage = location.href ;

  if(title==null)
  { h = document.getElementsByTagName("title") ;
    if(h.length==0) alert('no title') ; else pagetitle = h[0].textContent ; 
  }
  else
  { pagetitle = title ; 
    h = document.getElementsByTagName('title') ;
    if(h.length==0) 
    { h = document.createElement('title') ;
      head.appendChild(h) ; 
    }
    else { h = h[0] ; while(h.firstChild) h.removeChild(h.firstChild) ; }
    h.appendChild(document.createTextNode(title)) ;
  }
  if(ic!=undefined&&ic!=null) 
  { h = document.createElement('link') ;
    h.setAttribute('rel','shortcut icon') ; 
    h.setAttribute('href',ic) ; 
    head.appendChild(h) ;
  }

  prevind = null ;
  here = { name:null , 
           serialno:null , 
           ind:null , 
           ncol:0 , 
           full:queryfullscreen() } ;

  if((ind=thispage.lastIndexOf('/'))>=0) thispage = thispage.substring(ind+1) ; 
  prev = document.referrer ;
  if((ind=prev.lastIndexOf('/'))>=0) prev = prev.substring(ind+1) ; 

  // set up the array nitem containing the number of items in each block
  for(nitem=[],ind=-1;ind<list.length;ind=k) 
  { for(k=ind+1;k<list.length&&list[k].name!=undefined;k++) ; 
    if(k>ind+1) nitem.push(k-(ind+1)) ; 
  }

  // preprocess the list
  if(maxthumb==null)
    maxthumb = setthumbshape(list,sizes,thumbshape,imagedir,hithumb) ; 

  // find retpage: index of first legal value in list, or null if none
  for(k=0;
      k<list.length && (list[k].name==undefined 
                    || list[k].retpage==undefined || list[k].retpage==null) ;
      k++) ;
  if(k<list.length) retpage = k ; else retpage = null ;

  // parse the url options for the current page
  if((ind=thispage.indexOf('?'))>=0)
  { query = optionparse(thispage.substring(ind+1)) ; 
    thispage = thispage.substring(0,ind) ; 
    ind = findimage(list,query.name,query.serialno) ;
  } 

  window.onresize = pixresize ;
  document.onkeydown = navigate ; 
  document.addEventListener('touchstart',startswipe,false) ;        
  document.addEventListener('touchmove',midswipe,false) ;        
  document.addEventListener('touchend',endswipe,false) ;
  if(ind<0||ind>=list.length) { fromnotes = 0 ; tabulate() ; }
  else 
  { fromnotes = (query.mode.charAt(0)=='n'&&retpage!=null) ; display(ind) ; }
}
/* -------------------------------------------------------------------------- */
/*page*/
/* ---------------------------------- swipes -------------------------------- */

function simulate(btn)
{ navigate({keyCode:btn,preventDefault:function(){}}) ; }

var xloc=null,yloc=null,xstart=null,ystart=null,fingersep=null,startsep=null ; 
var swipetime=null ;

function startswipe(e) 
{ var x0=e.touches[0].clientX,y0=e.touches[0].clientY,x1,y1 ;
  if(e.touches.length==1) 
  { xstart = x0 ; ystart = y0 ; swipetime = new Date().getTime() ; } 
  else if(e.touches.length==2&&here.ncol==0)
  { e.preventDefault() ;        // don't let iOS take control of pinches
    x1 = e.touches[1].clientX ;
    y1 = e.touches[1].clientY ;
    startsep = Math.sqrt((x1-x0)*(x1-x0)+(y1-y0)*(y1-y0)) ;
  }
}
function midswipe(e) 
{ var x0=e.touches[0].clientX,y0=e.touches[0].clientY,x1,y1 ;
  if(e.touches.length==1) 
  { xloc = x0 ; yloc = y0 ; fingersep = startsep = null ; } 
  else if(e.touches.length==2&&here.ncol==0)
  { e.preventDefault() ; 
    x1 = e.touches[1].clientX ;
    y1 = e.touches[1].clientY ;
    fingersep = Math.sqrt((x1-x0)*(x1-x0)+(y1-y0)*(y1-y0)) ;
    xloc = yloc = xstart = ystart = swipetime = null ; 
  }
}
function endswipe(e) 
{ var v ;
  if(fingersep!=null&&startsep!=null&&here.ncol==0)
  { e.preventDefault() ; 
    if(fingersep>startsep+50) simulate(38) ; 
    else if(startsep>fingersep+50) simulate(40) ; 
  }
  else if(xloc!=null&&yloc!=null&&xstart!=null&&ystart!=null&&swipetime!=null)
  { swipetime = new Date().getTime() - swipetime ; 
    v = Math.sqrt((xloc-xstart)*(xloc-xstart)+(yloc-ystart)*(yloc-ystart)) ;
    if(v>0.65*swipetime)
    { e.preventDefault() ; 
      if(Math.abs(xloc-xstart)>100&&Math.abs(yloc-ystart)<100)
      { if(xloc>xstart) simulate(37) ; else simulate(39) ; }
      else if(yloc>ystart+100&&Math.abs(xloc-xstart)<100) simulate(13) ; 
    }
  }
  xloc = yloc = xstart = ystart = fingersep = startsep = swipetime = null ; 
}
/* -------------------------------------------------------------------------- */

function tabinfodiv(list)
{ var d=document.createElement('div'),i,npix,nsect,nhidden,s,noraw='' ; 
  for(npix=nsect=nhidden=i=0;i<list.length;i++)
  { if(list[i].name==undefined) nsect += 1 ; 
    else 
    { npix += 1 ; 
      if(list[i].display=='none') nhidden += 1 ; 
      if(i==0) nsect += 1 ; 
    }
    if( list[i].rawshape!=undefined && list[i].rawshape!=null
     && list[i].rawshape.length==2
     && ( list[i].rawshape[0]==0||list[i].rawshape[1]==0) ) 
      noraw += ' ' + list[i].name ;
  }

  if(nsect>1) 
  { d.appendChild(document.createTextNode(nsect+' sections')) ;
    d.appendChild(document.createElement('br')) ;
  }
  s = npix + ' photos' ;
  if(nhidden) s += ' (' + (npix-nhidden) + ' visible, ' + nhidden + ' hidden)' ;
  d.appendChild(document.createTextNode(s)) ;
  d.appendChild(document.createElement('br')) ;

  if(noraw!='')
  { d.appendChild(document.createTextNode('No raw shape for'+noraw)) ;
    d.appendChild(document.createElement('br')) ;
  }
  return d ; 
}

• enterfullscreen     • queryfullscreen     • querycanfullscreen     • reluri     • genobject     • getxmlsize     • geturialias     • getphotolist     • thumbind     • retlink     • fullcaption     • uncaption     • setthumbshape     • imgshape     • imgsize     • sparepix     • jpg     • srcset     • preload     • getsize     • genimage     • setimgpos     • findimage     • setimg     • function     • rescale     • resizesub     • maketextdiv     • enredcell     • navcell     • thumb     • setsize     • genloadhandler     • resize     • enlarge     • reduce     • gendisplay     • pixinfodiv

// www.masterlyinactivity.com/software/pix.html
/* The MIT License

   Copyright (c) 2016, by Colin Champion <colin.champion@masterlyinactivity.com>

   Permission is hereby granted, free of charge, to any person obtaining
   a copy of this software and associated documentation files (the
   "Software"), to deal in the Software without restriction, including
   without limitation the rights to use, copy, modify, merge, publish,
   distribute, sublicense, and/or sell copies of the Software, and to
   permit persons to whom the Software is furnished to do so, subject to
   the following conditions:

   The above copyright notice and this permission notice shall be
   included in all copies or substantial portions of the Software.

   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
   EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
   MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
   NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
   BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
   ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
   CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
   SOFTWARE.
*/
var pixlib = 1 ; 
var captionbox = null ; 

/* -------------------------------------------------------------------------- */

function enterfullscreen() 
{ if(document.documentElement.requestFullscreen) 
    document.documentElement.requestFullscreen() ;
  else if(document.documentElement.mozRequestFullScreen) 
    document.documentElement.mozRequestFullScreen() ;
  else if(document.documentElement.webkitRequestFullscreen) 
    document.documentElement.webkitRequestFullscreen() ;
  else if(document.documentElement.msRequestFullscreen) 
    document.documentElement.msRequestFullscreen() ;
}
function queryfullscreen() 
{ if(document.fullScreen||document.mozFullScreen||document.webkitIsFullScreen)
    return 1 ; 
  else return 0 ;
}
function querycanfullscreen()
{ if ( document.documentElement.requestFullscreen
    || document.documentElement.mozRequestFullScreen
    || document.documentElement.webkitRequestFullscreen 
    || document.documentElement.msRequestFullscreen ) return 1 ; 
  else return 0 ;
}
/* ---------------------------- relative uri  ------------------------------- */

function reluri(u1,u2) 
{ if(u1==null||u1==undefined||u2==null||u2==undefined) return u2 ; 
  var u = u2.toLowerCase() ; 
  if( u.substring(0,7)=="http://" || u.substring(0,7)=="file://"
   || u.substring(0,8)=="https://" ) return u2 ;
  var last = u1.lastIndexOf('/') ; 
  if(last<0) return u2 ; 
  u1 = u1.substring(0,last) ; 

  while(u2.substring(0,3)=='../')
  { last = u1.lastIndexOf('/') ; 
    if(last<0) return u2 ; 
    u2 = u2.substring(3) ; 
    u1 = u1.substring(0,last) ; 
  }
  return u1 + '/' + u2 ; 
}
/* -------------------------------------------------------------------------- */

function genobject(node,s0,s1,s2,s3,s4,s5,s6,s7,s8,s9,s10)
{ var i,v,l=[s0,s1,s2,s3,s4,s5,s6,s7,s8,s9,s10],r={},ind ;
  for(i=0;i<l.length&&l[i]!=undefined;i++) 
  { v = node.getAttribute(l[i]) ; 
    if((l[i]=='scale'||l[i]=='fontsize')&&v!=null&&v!='') v = parseFloat(v) ; 
    else if(l[i]=='hithumb'&&v!=null&&v!='')
    { ind = v.indexOf(',') ; 
      if(ind<0) { alert('Badly formed hithumb: '+v) ; throw '' ; }
      v = { suffix:v.substring(0,ind) , scale:parseFloat(v.substring(ind+1)) } ;
    }
    if(v!=null&&(v!=''||l[i]=='suffix')) r[l[i]] = v ; 
  }
  return r ;
}
/* -------------------------------------------------------------------------- */

function getxmlsize(item,aliases)
{ var i,j,w ; 
  if(item==null) return null ;
  if(item.charAt(0)=='[')
  { for(i=1;i<item.length&&item.charAt(i)==' ';i++) ;
    for(j=i;j<item.length&&item.charAt(j)!=','&&item.charAt(j)!=' ';j++) ;
    if(j==item.length) { alert("badly formed shape: "+item) ; throw '' ; }
    w = parseInt(item.substring(i,j)) ;
    for(i=j;i<item.length&&item.charAt(i)!=',';i++) ;
    for(j=i;j<item.length&&item.charAt(j)!=']';j++) ;
    if(j==item.length) { alert("badly formed shape: "+item) ; throw '' ; }
    return [ w , parseInt(item.substring(i+1,j)) ] ;
  }
  if(aliases!=undefined) for(i=0;i<aliases.length;i++)
    if(item==aliases[i].name) return aliases[i].shape ;
  alert("shape "+item+" not defined") ; 
  throw '' ; 
}
/* -------------------------------------------------------------------------- */

function geturialias(item,field,aliases)
{ var k,alias,uri ;
  if(item==null||item[field]==undefined||item[field]==null) return ;
  k = item[field].indexOf(',') ; 
  if(k<=0) return ; 
  alias = item[field].substring(0,k) ;
  uri = item[field].substring(k+1) ; 
  for(k=0;k<aliases.length&&
          (aliases[k].name!=alias||aliases[k].uri==null);k++) ;
  if(k==aliases.length) return ; 
  if(aliases[k].uri.charAt(aliases[k].uri.length-1)!='/'&&uri.charAt(0)!='/') 
    item[field] = aliases[k].uri + '/' + uri ; 
  else item[field] = aliases[k].uri + uri ; 
}
/* -------------------------------------------------------------------------- */

function getphotolist(xmldoc,baseuri)
{ var tree,i,kids,j,k,links=[],hithumb=null,pixp=null,imagedir=null,maxt ;
  var aliases=[],ll=[],ss=[],thumbshape=null,maxthumb=null,title=null,ic=null ;
  var sh={shape:null,rawshape:null,thumbshape:null} ; 
  var ur={href:null,gps:null,retpage:null,imagedir:null} ;

  // extract fields
  tree = xmldoc.getElementsByTagName('photolist')[0].childNodes ;
  for(i=0;i<tree.length;i++)
  { if(tree[i].nodeName=='link')
      links.push(genobject(tree[i],'name','href')) ;
    else if(tree[i].nodeName=='title') title = tree[i].textContent ; 
    else if(tree[i].nodeName=='hithumb')
      hithumb = genobject(tree[i],'suffix','scale') ;
    else if(tree[i].nodeName=='icon') ic = tree[i].getAttribute('href') ;
    else if(tree[i].nodeName=='pixpage') pixp = tree[i].getAttribute('href') ;
    else if(tree[i].nodeName=='imagedir') 
      imagedir = tree[i].getAttribute('href') ;
    else if(tree[i].nodeName=='thumbshape') 
      thumbshape = tree[i].getAttribute('shape') ;
    else if(tree[i].nodeName=='alias') 
      aliases.push(genobject(tree[i],'name','shape','uri')) ;
    else if(tree[i].nodeName=='sizes') 
    { for(kids=tree[i].childNodes,j=0;j<kids.length;j++)
        if(kids[j].nodeName=='size')
          ss.push(genobject(kids[j],'suffix','scale','type','fontsize')) ;
    }
    else if(tree[i].nodeName=='section') 
    { ll.push(genobject(tree[i],'title','gps')) ;
      for(kids=tree[i].childNodes,j=0;j<kids.length;j++)
        if(kids[j].nodeName=='img') ll.push(genobject
          (kids[j],'name','title','shape','rawshape','thumbshape','caption',
                   'display','hithumb','retpage','retid','imagedir')) ;
    }
    else if(tree[i].nodeName=='img') ll.push(genobject
          (tree[i],'name','title','shape','rawshape','thumbshape','caption',
                   'display','hithumb','retpage','retid','imagedir')) ;
  }

  // process aliases
  for(i=0;i<aliases.length;i++) 
    aliases[i].shape = getxmlsize(aliases[i].shape) ;
  for(i=0;i<ll.length;i++) 
  { for(j in sh)
      if(ll[i][j]!=undefined) ll[i][j] = getxmlsize(ll[i][j],aliases) ;
    for(j in ur) geturialias(ll[i],j,aliases) ; 
  }
  if(thumbshape!=null) thumbshape = getxmlsize(thumbshape,aliases) ;
  for(i=0;i<links.length;i++) for(j in ur) geturialias(links[i],j,aliases) ; 
  geturialias(pixp,'href',aliases) ; 
  geturialias(ic,'href',aliases) ; 
  geturialias(imagedir,'href',aliases) ; 
  maxt = setthumbshape(ll,ss,thumbshape,imagedir,hithumb) ;

  if(baseuri!=null&&baseuri!=undefined)
  { pixp = reluri(baseuri,pixp) ;
    for(i=0;i<ll.length;i++) 
    { ll[i].filename = reluri(baseuri,ll[i].filename) ; 
      ll[i].gps = reluri(baseuri,ll[i].gps) ; 
    }
    for(i=0;i<links.length;i++) links[i].href = reluri(baseuri,links[i].href) ; 
  }

  return { title:title , icon:ic , list:ll , sizes:ss , links:links , 
           pixpage:pixp , maxthumb:maxt } ;
}
/* -------------------------------------------------------------------------- */

function thumbind(sizes)
{ var ind ;
  for(ind=0;ind<sizes.length&&sizes[ind].type!='thumb';ind++) ;
  if(ind==sizes.length) { alert('no sizes entry of type "thumb"') ; throw '' ; }
  return ind ;
}
/* -------------------------------------------------------------------------- */

function retlink(list,ind)
{ var i,k,blink ;
  for(i=ind;i>=0&&(list[i].retpage==undefined||list[i].retpage==null);i--) ; 
  if(i<0) return null ; ; 
  blink = list[i].retpage+'.html' ;
  for(k=ind;k>=i&&(list[k].retid==undefined||list[k].retid==null);k--) ;
  if(k>=i) blink += '#' + list[k].retid ;
  return blink ;
}
/* -------------------------------------------------------------------------- */

function fullcaption(caption,xpos,ypos)
{ captionbox = document.createElement('div') ; 
  captionbox.setAttribute('style','position:fixed;background:white;bottom:'+
          (window.innerHeight-ypos)+';left:'+xpos+';padding:8;opacity:0.9;'+
           'font-family:helvetica') ;
  captionbox.appendChild(document.createTextNode(caption)) ;
  document.getElementsByTagName("body")[0].appendChild(captionbox) ; 
  setTimeout(uncaption,1000+caption.length*10) ;
}
function uncaption()
{ if(captionbox!=null) 
  { document.getElementsByTagName("body")[0].removeChild(captionbox) ; 
    captionbox = null ; 
  }
}
/* -------------------------------------------------------------------------- */
/*page*/
/* ---- fill in missing thumbs and raws, return maximum thumb dimensions ---- */

function setthumbshape(list,sizes,thumbshape,imagedir,hithumb)
{ var ind,end,i,r0,r1,maxthumb,k,umax,doraw,prevdir=null,sortlist,nsort ;
  if(typeof thumbshape=='undefined'||thumbshape==undefined) thumbshape = null ; 
  if(typeof imagedir=='undefined'||imagedir==undefined) imagedir = null ; 
  if(typeof hithumb=='undefined'||hithumb==undefined) hithumb = null ; 

  ind = thumbind(sizes) ; 
  if(thumbshape==null||thumbshape.length!=2)
  { if(sizes[ind].scale>0) r0 = sizes[ind].scale / sizes[0].scale ;
    else { alert('thumbs have size '+sizes[ind].scale) ; throw '' ; }
  }

  for(doraw=r1=ind=0;ind<sizes.length&&sizes[ind].type!='raw';ind++) ;
  if(ind<sizes.length) { r1 = sizes[ind].scale / sizes[0].scale ; doraw = 1 ; }

  // sort photos by name to check for dupes
  sortlist = new Array(list.length) ;
  for(nsort=ind=0;ind<list.length;ind++) if(list[ind].name!=undefined) 
    sortlist[nsort++] = { name:list[ind].name , ind:ind } ;
  sortlist.sort(function(a,b)
    { var ret = a.name.localeCompare(b.name) ; 
      if(ret!=0) return ret ; else return a.ind - b.ind ;
    } ) ;
  // now inject a serialno field into the list
  for(ind=0;ind<nsort;ind=end)
    for(end=ind;end<nsort&&sortlist[end].name==sortlist[ind].name;end++) 
      list[sortlist[end].ind].serialno = end - ind ; 

  // now pass through the list filling in missing fields
  for(maxthumb=[0,0],ind=0;ind<list.length;ind++) if(list[ind].name!=undefined) 
  { // thumbshape/hithumb/maxthumb
    if(list[ind].thumbshape==undefined||list[ind].thumbshape==null)
    { if(thumbshape!=null&&thumbshape.length==2) 
        list[ind].thumbshape = thumbshape ;
      else for(list[ind].thumbshape=[0,0],k=0;k<2;k++) 
        list[ind].thumbshape[k] = Math.floor(0.5+list[ind].shape[k]*r0) ; 
    }
    else if(list[ind].hithumb==undefined||list[ind].hithumb==null) 
      list[ind].hithumb = hithumb ;
    for(k=0;k<2;k++) if(list[ind].thumbshape[k]>maxthumb[k]) 
      maxthumb[k] = list[ind].thumbshape[k] ; 

    // rawshape
    if(doraw&&(list[ind].rawshape==undefined||list[ind].rawshape==null))
      for(list[ind].rawshape=[0,0],k=0;k<2;k++) 
        list[ind].rawshape[k] = Math.floor(0.5+list[ind].shape[k]*r1) ; 

    // filename
    if(list[ind].imagedir!=undefined||list[ind].imagedir!=null)
    { prevdir = list[ind].imagedir ; 
      list[ind].filename = prevdir + '/' + list[ind].name ;
    }
    else if(imagedir!=null) 
      list[ind].filename = imagedir + '/' + list[ind].name ;
    else if(prevdir!=null) 
      list[ind].filename = prevdir + '/' + list[ind].name ;
    else list[ind].filename = list[ind].name ;
  }

  // fill in the fontsize field in sizes
  for(R=k=ind=0;ind<sizes.length;ind++) 
    if(sizes[ind].type==undefined||sizes[ind].type==null) 
  { R += Math.log(sizes[ind].scale) ; k += 1 ; }
  R /= k ;

  for(ind=0;ind<sizes.length;ind++) 
    if( (sizes[ind].type==undefined||sizes[ind].type==null)
     && (sizes[ind].fontsize==undefined||sizes[ind].fontsize==null) ) 
      sizes[ind].fontsize = 16 * Math.exp((Math.log(sizes[ind].scale)-R)/3) ;

  for(umax=null,ind=0;ind<sizes.length;ind++) 
    if(sizes[ind].type==undefined||sizes[ind].type==null)
      if(umax==null||sizes[ind].fontsize>umax) umax = sizes[ind].fontsize ;
  for(ind=0;ind<sizes.length;ind++) if(sizes[ind].type=='raw') 
    if(sizes[ind].fontsize==undefined||sizes[ind].fontsize==null)
      sizes[ind].fontsize = umax ; 

  return maxthumb ;
}
/* ----------------------------- image functions ---------------------------- */

function imgshape(item,sizes,sizeno)
{ if(sizes[sizeno].type=='thumb') return item.thumbshape ; 
  else if(sizes[sizeno].type=='raw') return item.rawshape ; 
  return [ Math.floor(0.5+item.shape[0]*sizes[sizeno].scale/sizes[0].scale) ,
           Math.floor(0.5+item.shape[1]*sizes[sizeno].scale/sizes[0].scale) ] ;
}
function imgsize(item,sizes,sizeno) { return imgshape(item,sizes,sizeno) ; } 

/* -------------------------------------------------------------------------- */
/*page*/
/* ----- sparepix finds the margins left if item is displayed at usesize ---- */

function sparepix(item,sizes,sizeno)
{ var shape=imgsize(item,sizes,sizeno),w=shape[0],h=shape[1],r ;
  h += Math.floor(0.5+1.25*sizes[sizeno].fontsize) + 2 ;
  if(item.caption!=undefined&&item.caption!=null) 
    h += Math.floor(0.5+sizes[sizeno].fontsize) + 2 ;

  r = [ window.innerHeight-50-h , window.innerHeight-h ] ;
  if(window.innerWidth-w<r[0]) r[0] = window.innerWidth-w ;
  if(window.innerWidth-w-50<r[1]) r[1] = window.innerWidth-w-50 ;
  return r ;
}
/* ---------------------- construct the jpg path name ----------------------- */

function jpg(item,sizes,sizeno)
{ return item.filename + sizes[sizeno].suffix + '.jpg' ; }

/* ------------------- generate the srcset for an image --------------------- */

function srcset(item,sizes,sizeno)
{ var i,s,hith,scale ;
  if(sizes[sizeno].type=='raw') return '' ;

  if(sizes[sizeno].type=='thumb') 
  { hith = item.hithumb ;
    if(hith!=undefined&&hith!=null )
      return item.filename + hith.suffix + '.jpg ' + hith.scale + 'x' ;
    if( item.thumbshape[0]*item.shape[1] != item.thumbshape[1]*item.shape[0] ) 
      return '' ;
    scale = ( sizes[0].scale * item.thumbshape[0] ) / item.shape[0] ;
  }
  else scale = sizes[sizeno].scale ;

  for(s='',i=0;i<sizes.length;i++) 
    if((sizes[i].type==undefined||sizes[i].type==null)&&(sizes[i].scale>scale))
  { if(s!='') s += ', ' ;
    s += jpg(item,sizes,i) + ' ' +
           (sizes[i].scale/scale).toFixed(1) + "x" ;
  }
  return s ;
}
/* -------------------------------------------------------------------------- */

function preload(item,sizes,sizeno,loadaction) 
{ return genimage(item,sizes,sizeno,loadaction) ; }

/* ------- getsize finds the largest image size which fits the screen ------- */

function getsize(item,sizes,loadstatus,thresh) 
{ var i,ibest,ismall,spare ;

  for(ismall=ibest=null,i=0;i<sizes.length;i++) 
    if( (sizes[i].type==undefined||sizes[i].type==null)
     && (loadstatus==undefined||loadstatus[i]>=thresh) )
  { if(ismall==null||sizes[i].scale<sizes[ismall].scale) ismall = i ; 
    spare = sparepix(item,sizes,i) ;
    if(spare[0]>=0||spare[1]>=0)
      if(ibest==null||sizes[i].scale>sizes[ibest].scale) ibest = i ; 
  }
  if(ibest==null) return ismall ; else return ibest ;
}
/* -------------------------------------------------------------------------- */
/*page*/
/* --------------------------------- genimage ------------------------------- */

function genimage(item,sizes,sizeno,loadfunc) 
{ var img=document.createElement('img') , shape , s ;
  if(sizeno==undefined||sizeno==null||sizeno<0) sizeno = getsize(item,sizes) ;
  shape = imgshape(item,sizes,sizeno) ; 
  img.setAttribute('width',shape[0]) ; 
  img.setAttribute('height',shape[1]) ; 
  if((s=srcset(item,sizes,sizeno))!='') img.setAttribute("srcset",s) ; 
  if(loadfunc!=undefined&&loadfunc!=null) img.onload = loadfunc ;
  img.setAttribute("src",jpg(item,sizes,sizeno)) ; 
//  console.log(img.src+(s==""?"":' ['+s+']')) ;
  return img ;
}
/* ------- setimgpos sets the image position according to window size ------- */

function setimgpos(parms,shape,caption)
{ var imgw=shape[0],imgh=shape[1],x,y,s,overflowx,overflowy ;
  var pad=parms.headh + parms.caph ;
  // set space to the amount of room we've got for the image div
  var space = [ window.innerWidth , window.innerHeight-pad ] ;
  if(parms.portrait) space[0] -= 50 ; else space[1] -= 50 ; 

  // set imgw and imgh to the desired dimensions of the image div
  if(imgh>space[1]) imgw += 20 ; // generous scrollbar
  if(imgw>space[0]) imgh += 20 ; 

  // set (x,y) to the coords of the main div
  x = (window.innerWidth-imgw)/2 ;
  y = (window.innerHeight-imgh)/2 - parms.headh;
  if(parms.portrait) { if(x<50) x = 50 ; if(y<0) y = 0 ; }
  else { if(x<0) x = 0 ; if(y<50) y = 50 ; } 
  x = Math.floor(x) ; 
  y = Math.floor(y) ; 

  // now constrain imgw, imgh by the window size
  if(imgw>window.innerWidth-x) imgw = window.innerWidth - x ;
  if(imgh>window.innerHeight-y-pad) imgh = window.innerHeight-y-pad ;

  s = 'position:absolute;left:'+x+'px;width:'+(window.innerWidth-x)+'px;top:' ;
  s += y+'px;height:'+(pad+imgh)+'px;overflow:hidden' ;
  parms.maindiv.setAttribute('style',s) ; 

  s = 'font-family:helvetica;color:silver;position:absolute;' ;
  s += 'left:0;width:'+(window.innerWidth-x)+'px;top:0;' ;
  s += 'height:'+parms.headh+'px;overflow:hidden;text-overflow:ellipsis;' ; 
  s += 'white-space:nowrap;font-size:'+parms.headf+'px' ;
  parms.headdiv.setAttribute('style',s) ; 

  if(parms.capdiv!=null)
  { s = 'font-family:helvetica;color:silver;position:absolute;' ;
    s += 'left:0;width:'+(window.innerWidth-x)+'px;bottom:0;' ;
    s += 'height:'+parms.caph+'px;overflow:hidden;font-size:'+parms.capf+'px;' ;
    s += 'text-overflow:ellipsis;white-space:nowrap' ;
    parms.capdiv.setAttribute('style',s) ; 
    parms.capdiv.setAttribute('onmouseover',
                     'fullcaption("'+caption+'",'+x+','+(pad+imgh+y)+")") ; 
  }

  if(imgw<shape[0]) overflowx = 'scroll' ; else overflowx = 'hidden' ; 
  if(imgh<shape[1]) overflowy = 'scroll' ; else overflowy = 'hidden' ; 
  s = 'position:absolute;left:0;width:'+imgw+'px;top:'+parms.headh+'px;' ;
  s += 'height:'+imgh+'px;overflow-x:'+overflowx+';overflow-y:'+overflowy ;
  parms.imgdiv.setAttribute('style',s) ; 
}
/* -------------------------------------------------------------------------- */
/*page*/
/* -------------------- find the list ind with a given name ----------------- */

function findimage(list,name,serialno)
{ var ind ; 
  if(serialno==undefined||serialno==null) serialno = 0 ; 
  for(ind=0;ind<list.length;ind++) 
    if(list[ind].name==name&&list[ind].serialno==serialno) return ind ; 
  return null ;
}
/* ---- adjust element attributes in accordance with a change of img size --- */

function setimg(parms,item,sizes,sizeno,fetchitem,preloadstatus)
{ var font,spare,shape,img,s,i,fetcher ; 

  if(sizeno==null) return ;
  enredcell(parms.enlink,sizes,sizeno,2) ;
  enredcell(parms.redlink,sizes,sizeno,-1) ;

  spare = sparepix(item,sizes,sizeno) ;
  shape = imgsize(item,sizes,sizeno) ;
  font = sizes[sizeno].fontsize ;
  parms.headh = 2 + Math.floor(0.5+1.25*font) ;
  parms.headf = Math.floor(0.5+font) ;
  if(item.caption==undefined||item.caption==null) 
    parms.caph = parms.capf = 0 ; 
  else
  { parms.caph = 2+Math.floor(0.5+font) ; 
    parms.capf = Math.floor(0.5+0.8*font) ;
  }

  parms.portrait = spare[0]<spare[1] ;

  // create the image for the new size
  fetcher = null ; 
  if(fetchitem!=undefined&&fetchitem!=null) 
    if(preloadstatus[i=getsize(fetchitem,sizes)]==0)  
  { preloadstatus[i] = 1 ; 
    fetcher = function() { genimage(fetchitem,sizes,i) ; } ;
  }
  img = genimage(item,sizes,sizeno,fetcher) ; 

  if(parms.img==null) parms.imgdiv.appendChild(img) ;
  else parms.imgdiv.replaceChild(img,parms.img) ;
  parms.img = img ; 
  setimgpos(parms,shape,item.caption) ;
  return sizeno ;
}
/* -------------------------------------------------------------------------- */
/*page*/
/* ------------ rescale finds the next larger/next smaller image ------------ */

function rescale(sizes,sizeno,dir)
{ var i,ind ;

  if(dir<0) 
  { for(ind=null,i=0;i<sizes.length;i++) 
      if(sizes[i].type==undefined||sizes[i].type==null)
        if(sizes[i].scale<sizes[sizeno].scale||sizes[sizeno].type=='raw')
          if(ind==null||sizes[i].scale>sizes[ind].scale) ind = i ;
    return ind ;
  }

  if(sizes[sizeno].type=='raw') return null ; 
  for(ind=null,i=0;i<sizes.length;i++) 
    if( sizes[i].type==undefined || sizes[i].type==null
     || (sizes[i].type=='raw'&&dir==2) )
      if( sizes[i].scale>sizes[sizeno].scale || sizes[i].type=='raw' )
  { if(ind==null) ind = i ; 
    else if(sizes[ind].type=='raw') ind = i ; 
    else if(sizes[i].scale<sizes[ind].scale&&sizes[i].type!='raw') ind = i ;
  }
  return ind ;
}
/* --------------- reposition image in response to a window resize ---------- */

function resizesub(iparms,dparms)
{ var k2,k0,dir,flag=0,shape,fetch,item=iparms.item,sizes=iparms.sizes ;
  var sizeno=iparms.sizeno ;
  k2 = getsize(item,sizes,iparms.loadstatus,2) ; 
  k0 = getsize(item,sizes) ; 

  // there's a better size already loaded
  if(iparms.holdsize==0&&k2!=sizeno) 
  { setimg(dparms,item,sizes,k2,iparms.fetchitem,iparms.preloadstatus) ; 
    iparms.sizeno = k2 ;
    return ; 
  } 
  // there's a better size not yet loaded
  else if(iparms.holdsize==0&&k0!=sizeno&&iparms.loadstatus[k0]==0) flag = 1 ; 
  else if(iparms.holdsize!=0&&k0==sizeno) iparms.holdsize = 0 ;
  else if(iparms.holdsize==0&&iparms.window!=null)
  { if( window.innerWidth>=iparms.window[0]
     && window.innerHeight>=iparms.window[1] ) dir = 1 ;
    else if( window.innerWidth<=iparms.window[0]
          && window.innerHeight<=iparms.window[1] ) dir = -1 ;
    else dir = null ; 
    if(dir!=null&&(k0=rescale(sizes,sizeno,dir))!=null) 
      if(iparms.loadstatus[k0]==0) flag = 1 ; 
  }
  if(flag)
  { iparms.window = null ;
    iparms.loadstatus[k0] = 1 ; 
    genimage(item,sizes,k0,genloadhandler(iparms.loadstatus,k0)) ; 
  }
  // redisplay if portrait<->landscape makes a fit possible, else redraw
  shape = imgsize(item,sizes,sizeno) ;
  spare = sparepix(item,sizes,sizeno) ;
  if(dparms.portrait!=(spare[0]<spare[1])&&(spare[0]<0)!=(spare[1]<0))
    dparms.portrait = 1-dparms.portrait ;
  setimgpos(dparms,imgsize(item,sizes,sizeno),item.caption) ;
}
/* -------------------------------------------------------------------------- */
/*page*/
/* ---------------- create a div containing an image title ------------------ */

function maketextdiv(title)
{ var div=document.createElement('div') ;
  div.appendChild(document.createTextNode(title)) ; 
  div.setAttribute('style','font-family:helvetica;color:silver') ;
  return div ; 
}
/* --------------------- create/reset enlarge/reduce icon ------------------- */

function enredcell(td,sizes,sizeno,dir)
{ var a,astyle ;
  astyle = 'font-family:helvetica;font-weight:normal;text-decoration:none;' ;
  while(td.firstChild) td.removeChild(td.firstChild) ;

  if(rescale(sizes,sizeno,dir)!=null) 
  { a = document.createElement('a') ; 
    a.setAttribute('href',dir>0?'javascript:enlarge()':'javascript:reduce()') ; 
    a.setAttribute("title",dir>0?"enlarge [\u2191 key]":"reduce [\u2193 key]") ; 
    a.setAttribute("style",astyle) ; 
    a.appendChild(document.createTextNode(dir>0?'\u2295':'\u2296')) ;
    td.appendChild(a) ; 
  }
}
/* --------------------- create a cell for navigation icon ------------------ */

function navcell(link,dir,string)
{ var a,td=document.createElement('td'),s='center',astyle ;
  if(dir=='l') s = 'left' ; else if (dir=='r') s = 'right' ;
  astyle = 'font-family:helvetica;font-weight:normal;text-decoration:none;' ;
  s = "text-align:" + s + (dir==0?'':';font-size:20px;') ;
  s += 'text-align:center;vertical-align:middle;font-size:16px;' +
                'line-height:16px;width:16px;height:16px' ;
  td.setAttribute("style",s) ; 
  if(dir!=0) td.setAttribute("rowspan",3) ; 

  if(link!=null) 
  { a = document.createElement('a') ; 
    a.setAttribute('href',link) ; 
    if(dir>0) s = "next [\u2192 key]" ; 
    else if(dir==0) s = "back to " + string + " [\u21b5 key]" ;
    else s = "prev [\u2190 key]" ;
    a.setAttribute("title",s) ; 
    a.setAttribute("style",astyle) ; 
    if(dir>0) s = '>' ; else if(dir==0) s = '\u21b5' ; else s = '<' ;
    a.appendChild(document.createTextNode(s)) ;
    td.appendChild(a) ; 
  }
  return td ; 
}
/* -------------------------------------------------------------------------- */

var maxthumb,thumbshape,imagedir,hithumb,thind=null,sfx=null ;

function thumb(name,serialno) 
{ var ind,s ; 
  if(maxthumb==undefined||maxthumb==null) 
    maxthumb = setthumbshape(list,sizes,thumbshape,imagedir,hithumb) ;
  if(thind==null) thind = thumbind(sizes) ;
  if(sfx==null) sfx = sizes[thind].suffix ;

  if(null==(ind=findimage(list,name,serialno))) 
    alert('Missing image: '+name+(serialno>0?('('+serialno+')'):'')) ;
  s = srcset(list[ind],sizes,thind) ;
  document.write('<a href="'+pixpage+'?image='+name+
       (serialno>0?('&serialno='+serialno):'')+'&mode=n">'+
       '<img class=pix src="'+
       jpg(list[ind],sizes,thind)+'" width='+list[ind].thumbshape[0]+
       ' height='+list[ind].thumbshape[1]+' title="'+list[ind].title+
       ((s==''||s==null)?'':('" srcset="'+s)) + '"></a>') ; 
}
/* -------------------------------------------------------------------------- */

var drawparms=null,itemparms=null ; 

function setsize(k)  
{ if(itemparms==null||(k=rescale(itemparms.sizes,itemparms.sizeno,k))==null) 
    return ;
  uncaption() ; 
  itemparms.sizeno = setimg(drawparms,itemparms.item,itemparms.sizes,k) ; 
  itemparms.holdsize = 1 ;  
}
function genloadhandler(loadstatus,k) 
{ return function() { if(itemparms!=null) { loadstatus[k] = 2 ; resize() ; } } ;
}
function resize() { if(itemparms!=null) resizesub(itemparms,drawparms) ; } 
function enlarge() { setsize(2) ; }
function reduce() { setsize(-1) ; }

/* -------------------------------------------------------------------------- */

function gendisplay(element,item,sizes,llink,blink,rlink,fromstring,fetchitem) 
{ var i,sizeno,captioned,table,tr,td,a,utd,dtd ; 
  if(element==undefined||element==null) 
  { itemparms = drawparms = null ; return ; }
  sizeno = getsize(item,sizes) ;
  if(item.caption==undefined||item.caption==null) captioned = 0 ; 
  else captioned = 1 ; 

  itemparms = { item:          item,
                sizes:         sizes,
                sizeno:        sizeno,
                holdsize:      0,
                loadstatus:    new Array(sizes.length),
                fetchitem:     fetchitem,
                preloadstatus: new Array(sizes.length),
                window:        [ window.innerWidth , window.innerHeight ] 
              } ;

  for(i=0;i<sizes.length;i++) 
    itemparms.loadstatus[i] = itemparms.preloadstatus[i] = 0 ;
  itemparms.loadstatus[sizeno] = 2 ;

  // make the navigation table
  table = document.createElement('table') ;
  table.setAttribute('cellpadding',0) ; 
  table.setAttribute('cellspacing',0) ; 

  // the '<' link
  tr = document.createElement('tr') ;
  tr.appendChild(navcell(llink,-1)) ; 

  // the enlarge link
  utd = document.createElement('td') ;
  utd.setAttribute("style",'text-align:center;font-size:16px;' +
                           'line-height:16px;width:16px;height:16px') ; 
  tr.appendChild(utd) ; 
  table.appendChild(tr) ; 

  // the '>' link
  tr.appendChild(navcell(rlink,1)) ; 
  table.appendChild(tr) ; 

  // the return link
  tr = document.createElement('tr') ;
  tr.appendChild(navcell(blink,0,fromstring)) ; 
  table.appendChild(tr) ; 

  // the reduce link
  tr = document.createElement('tr') ;
  dtd = document.createElement('td') ;
  dtd.setAttribute("style",'text-align:center;font-size:16px;' +
                          'line-height:16px;width:16px;height:16px') ; 
  tr.appendChild(dtd) ; 
  table.appendChild(tr) ; 

  drawparms = { headh:     null, 
                caph:      null, 
                headf:     null, 
                capf:      null, 
                enlink:    utd,
                redlink:   dtd,
                img:       null,
                portrait:  null,
                maindiv:   document.createElement('div'), 
                headdiv:   maketextdiv(item.title),
                imgdiv:    document.createElement('div'),
                capdiv:    captioned==0?null:maketextdiv(item.caption)
              } ;

  setimg(drawparms,item,sizes,sizeno,fetchitem,itemparms.preloadstatus) ; 

  drawparms.imgdiv.appendChild(drawparms.img) ;
  drawparms.maindiv.appendChild(drawparms.headdiv) ; 
  drawparms.maindiv.appendChild(drawparms.imgdiv) ; 
  if(captioned) drawparms.maindiv.appendChild(drawparms.capdiv) ; 

  while(element.firstChild) element.removeChild(element.firstChild) ;
  element.appendChild(table) ; 
  element.appendChild(drawparms.maindiv) ; 
}
/* -------------------------------------------------------------------------- */

function pixinfodiv(list,i,sizes)
{ var d=document.createElement('div'),ind,hind,r,k,shape,s ;

  s = 'Name: ' + list[i].name ;
  if(list[i].serialno>0) s += '('+list[i].serialno+')' ;
  if(list[i].display=='none') s += ' [hidden]' ;
  d.appendChild(document.createTextNode(s)) ;
  d.appendChild(document.createElement('br')) ;

  d.appendChild(document.createTextNode('Title: '+list[i].title)) ;
  d.appendChild(document.createElement('br')) ;

  for(hind=null,ind=0;ind<i;ind++) if(list[ind].name==null) hind = ind ;
  if(hind!=null) 
  { d.appendChild(document.createTextNode('Section: '+list[hind].title)) ;
    d.appendChild(document.createElement('br')) ;
  }

  // how many shapes?
  for(r=ind=0;ind<sizes.length;ind++) 
    if(sizes[ind].type==undefined||sizes[ind].type==null) r += 1 ;
  s = 'Available in ' + r + ' size' + (r>1?'s: ':': ') ;

  // print the shapes
  for(k=ind=0;ind<sizes.length;ind++) 
    if(sizes[ind].type==undefined||sizes[ind].type==null)
  { if(k>0) { if(k==r-1) s += ' and ' ; else s += ', ' ; }
    shape = imgsize(list[i],sizes,ind) ;
    s += shape[0] + 'x' + shape[1] ;
    k += 1 ; 
  }
  d.appendChild(document.createTextNode(s)) ;
  d.appendChild(document.createElement('br')) ;

  // print the thumb shape
  shape = list[i].thumbshape ;
  s = 'Thumb: ' + shape[0] + 'x' + shape[1] ;
  hith = list[i].hithumb ;
  if(hith!=undefined&&hith!=null )
    s += ' (hi-res: '+ shape[0]*hith.scale + 'x' + shape[1]*hith.scale + ')' ;
  d.appendChild(document.createTextNode(s)) ;
  d.appendChild(document.createElement('br')) ;

  // print the raw shape
  for(ind=0;ind<sizes.length;ind++) if(sizes[ind].type=='raw')
  { shape = imgsize(list[i],sizes,ind) ;
    s = 'Raw: ' + shape[0] + 'x' + shape[1] ;
    d.appendChild(document.createTextNode(s)) ;
    d.appendChild(document.createElement('br')) ;
  }
  return d ; 
}

• getval     • main

#include "memory.h"

char *getval(char *str,char *tag)
{ int i,j,k,n ;
  char *p=strstr(str,tag),*q,*s ;
  if(p==0) return 0 ; 
  p += strlen(tag) ; 
  for(;p[0]!=0&&(p[0]==' '||p[0]==':');p++) ;
  if(p[0]==0) return 0 ; 

  // character string
  if(p[0]=='\"')
  { for(q=p+1;q[0]!=0&&q[0]!='\"';q++) ; 
    if(q[0]==0) return 0 ; 

    // reformat unicode
    for(n=0,s=p;s<q;s++) if(s[0]=='\\'&&s[1]=='u') n += 1 ; 
    s = charvector(2+2*n+(q-p)) ;
    for(j=0;p<=q;)
      if(p[0]=='\\'&&p[1]=='u') 
      { for(s[j++]='&',s[j++]='#',s[j++]='x',p+=2,k=0;k<4;k++) 
        { s[j++] = p[0] ; p++ ; }
        s[j++] = ';' ;
      }
    else { s[j++] = p[0] ; p++ ; }
    return s ;
  }

  // shape [w,h]
  if(p[0]=='[') { for(q=p+1;q[0]!=0&&q[0]!=']';q++) ; q++ ; }
  else for(q=p+1;q[0]!=0&&q[0]!=','&&q[0]!='}'&&q[0]!=' ';q++) ; 
  if(q[0]==0) return 0 ; 
  s = charvector(3+(q-p)) ;
  s[0] = '\"' ;
  strncpy(s+1,p,q-p) ;
  s[1+(q-p)] = '\"' ;
  s[2+(q-p)] = 0 ;
  return s ;
}

int main(int argc,char **argv)
{ int i,j,k,l,insect,finished,pos,len ;
  char *line,*s,*s0,*s1 ;
  char *imgtag[] = { "title","shape","rawshape","thumbshape","hithumb",
      "caption","display","retpage","retid","imagedir",0} ;
  FILE *ifl=fopenread(argv[1]),*ofl ;
  line = charvector(strlen(argv[1])+3) ;
  strcpy(line,argv[1]) ; 
  for(i=0;line[i]!=0&&line[i]!='.';i++) ; 
  for(j=0;j<5;i++,j++) line[i] = ".xml"[j] ;
  ofl = fopenwrite(line) ; 
  fprintf(ofl,"<photolist>\n") ;

  for(finished=insect=0;finished==0&&(line=freadline(ifl));free(line))
  { for(i=0;finished==0&&line[i];i++) if(line[i]==';') finished = 1 ; 
    for(i=0;line[i];i=j)
    { for(j=i+1;line[j]!=0&&line[j]!='=';j++) ;
      if(line[j]==0) break ;
      for(i=j-1;i>=0&&line[i]==' ';i--) ;
      if(i<0) break ; 
      line[i+1] = 0 ; 
      for(k=i-1;k>=0&&line[k]!=0&&line[k]!=' '&&line[k]!=',';k--) ;
      fprintf(ofl,"<alias name=\"%s\" shape=\"",line+k+1) ; 
      for(k=j+1;line[k]!=0&&line[k]==' ';k++) ; 
      if(line[k]==0) { fprintf(ofl,"\n") ; break ; }
      for(j=k+1;line[j]!=0&&line[j-1]!=']';j++) ; 
      line[j-1] = 0 ;
      fprintf(ofl,"%s]\"/>\n",line+k) ;
    }
  }

  for(insect=0;(line=freadline(ifl));free(line))
  { for(k=i=0;line[i];i++) 
      if(line[i]=='{') k |= 1 ; else if(line[i]=='}') k |= 2 ; 
    if(k==1)
    { s = freadline(ifl) ; 
      s0 = charvector(strlen(s)+strlen(line)+1) ;
      strcpy(s0,line) ; 
      strcat(s0,s) ; 
      free(s,line) ; 
      line = s0 ; 
    }
    // pixpage, imagedir
    if(strncmp(line,"pixpage",7)==0||strncmp(line,"imagedir",8)==0)
    { if(insect==1) { fprintf(ofl,"</section>\n\n") ; insect = 0 ; } 
      if(insect==2) { fprintf(ofl,"</sizes>\n\n") ; insect = 0 ; }
      if(line[0]=='p') fprintf(ofl,"<pixpage  href=") ; 
      else fprintf(ofl,"<imagedir href=") ; 
      for(i=0;line[i]&&line[i]!='\"';i++) ; 
      fprintf(ofl,"\"") ;
      for(i++;line[i]&&line[i]!='\"';i++) fprintf(ofl,"%c",line[i]) ;
      fprintf(ofl,"\"/>\n") ;
      continue ;
    }
    for(i=0;line[i]&&line[i]!='{';i++) ;
    if(line[i]==0) continue ; 
    // size, hithumb
    if((s=getval(line+i,"suffix")))
    { if(insect==1) { fprintf(ofl,"</section>\n\n") ; insect = 0 ; }
      if(insect==0&&line[0]!='h') fprintf(ofl,"<sizes>\n") ; 
      s0 = getval(line+i,"scale") ; 
      s1 = getval(line+i,"type") ; 
      if(line[0]=='h') fprintf(ofl,"<hithumb  suffix=%s scale=%s",s,s0) ;
      else { fprintf(ofl,"  <size suffix=%s scale=%s",s,s0) ; insect = 2 ; }
      free(s,s0) ; 
      if(s1) { fprintf(ofl," type=%s",s1) ; free(s1) ; }
      fprintf(ofl,"/>\n") ; 
    }
    // link
    else if((s=getval(line+i,"href")))
    { if(insect==1) { fprintf(ofl,"</section>\n\n") ; insect = 0 ; }
      if(insect==2) { fprintf(ofl,"</sizes>\n\n") ; insect = 0 ; }
      s0 = getval(line+i,"name") ;
      if(s0) fprintf(ofl,"<link     name=%s",s0) ; 
      len = 16 + strlen(s0) ; 
      if(len+strlen(s)>80) fprintf(ofl,"\n         ") ; 
      fprintf(ofl," href=%s/>\n",s) ;
      free(s,s0) ;
    }
    // img
    else if((s=getval(line+i,"name")))
    { if(insect) fprintf(ofl,"  ") ; 
      fprintf(ofl,"<img name=%s",s) ; 
      pos = 12 + strlen(s) ; 
      free(s) ;
      for(k=0;imgtag[k];k++) if((s=getval(line+i,imgtag[k]))) 
      { len = strlen(s) + strlen(imgtag[k]) + 1 ; 
        if(pos+len+1<80) { fprintf(ofl," %s=%s",imgtag[k],s) ; pos += len+1 ; }
        else 
        { fprintf(ofl,"\n  %s   %s=%s",insect?"  ":"",imgtag[k],s) ; 
          pos = len + (insect?7:5) ; 
        }
        free(s) ; 
      }
      fprintf(ofl,"/>\n") ; 
    }
    // section
    else if((s=getval(line+i,"title")))
    { if(insect==1) fprintf(ofl,"</section>\n") ; 
      if(insect==2) { fprintf(ofl,"</sizes>\n\n") ; insect = 0 ; }
      fprintf(ofl,"<section title=%s",s) ;
      pos = 20 + strlen(s) ; 
      free(s) ;
      if((s=getval(line+i,"gps"))) 
      { if(pos+strlen(s)>80) fprintf(ofl,"\n        ") ; 
        fprintf(ofl," gps=%s",s) ; 
        free(s) ; 
      }
      fprintf(ofl,">\n") ; 
      insect = 1 ; 
    }
  }
  fprintf(ofl,"</photolist>\n") ;
}

pix.js pixlib.js