Summary: pix.js comprises javascript code for displaying photo galleries which may be incorporated into webpages.

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

overview : organisation of this manual : creating a picture page : URL options : local use : contact me

pix.js provides the javascript which does all the work for HTML pages displaying photos in the manner of this website. It offers the following functions:

When you create a picture gallery using pix.js, you thereby create an HTML page containing a table of thumbnail images. The user can click on the thumbnails to see individual images (which are assigned separate URLs). Various controls are provided, allowing the user to obtain information about the gallery and about the individual photos, to enlarge and reduce the images, and to perform a few other functions.

To see what you have to do (besides providing the images) you can look at a sample HTML file, eg. the source code for our Lake Maggiore gallery, but you don’t learn much since the work is done elsewhere. The page says simply

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

The useful information is contained in the XML file list.xml, and looking at that will show you the work involved in setting up the HTML.

This page largely comprises a reference manual for pix.js. The next few sections explain the 6 lines of the sample HTML file in detail. If you’re interested in getting an overview of how to use pix.js you’ll be better off looking at the sample gallery. You will see there what information you need to provide. Understanding the format is a matter of detail. Some useful thoughts are contained in the section on reflections. And if you really want the details of constructing an XML gallery, they’re provided in a later section.

A photo gallery is specified by an XML configuration file (see below). Parallel to it (and preferably in the same directory) you need a 6-line HTML stub, which is simplicity itself and mostly boilerplate.

<html><head>
<meta charset=utf-8> 
<script src="https://www.masterlyinactivity.com/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="https://www.masterlyinactivity.com/pix.js"></script>

Instruction to the browser to incorporate pix.js (which in turn calls for pixlib.js).

 

</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 URL, 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; this no longer works.

A subsequent version used a javascript format for picture lists, included as a <script src="..."></script> in the html header. This is no longer supported.

 

</html>

Boilerplate. Obligatory.

The URL of your page will be eg.

https://www.masterlyinactivity.com/capeverde/pix.html

By appending options after a ‘?’ you can force a different view of the pictures. To view an individual picture you use the ‘image’ attribute followed by the image name, e.g.

https://www.masterlyinactivity.com/capeverde/pix.html?image=ribeira

To limit the view to pictures in a particular category, use the ‘cat’ option:

https://www.masterlyinactivity.com/capeverde/pix.html?cat=2017

To force the display of images whose display option is ‘none’, use the ‘mode’ option with an ‘a’ in its value:

https://www.masterlyinactivity.com/capeverde/pix.html?mode=a

Or to limit the display to starred images, use a ‘*’:

https://www.masterlyinactivity.com/capeverde/pix.html?mode=*

Or, finally, to display only the hidden images (i.e. those with display="none"), use an ‘h’:

https://www.masterlyinactivity.com/capeverde/pix.html?mode=h

Inclusion of a ‘v’ in the mode tells pix.js to perform a validation rather than displaying photos.

Options can be concatenated using ampersands:

https://www.masterlyinactivity.com/capeverde/pix.html?image=ribeira&cat=2017&mode=a

When developing a picture gallery you are urged to do so on your own computer, and not to upload to your website until it’s ready. You’ll encounter resistance from your browser, which will refuse to read the local XML configuration file owing to the internet’s stupid ‘same origin policy’. Firefox didn’t complain until fairly recently (2019), but you can placate it by setting the parameter security.fileuri.strict_origin_policy to false in about:config.

Email me at colin·champion&routemaster·app, substituting full stops for the dots and an ampersat for the ampersand. I don’t guarantee to make changes but I’m happy to hear from you.

overview : organisation of this manual : creating a picture page : URL options : local use : contact me

using pix.js : general reflections : srcsets : aspect ratios : thumbnails : scale factors : retpage/retid

pix.js comprises software for display of a library of images. This software does not prepare your input data: you have to do it yourself. This is because the tasks involved are not suitable for implementation through web browsers. However two related functions are suitable for browsers, and these are provided.

The data preparation for pix.js is fairly onerous. You need to make a version of each image in at least two sizes (thumbnail and a regular size), but I often provide 6 or 7. The XML configuration files may come across as disagreeably “tecky” to naive users.

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.

pix.js is intended for web pages, but you may access the same pages from your own disc (by file load or the file:// protocol): see above for some guidance.

You should not rely on pix.js without letting me know (see above). It was written for my own use, and while I try hard to preserve compatibility, I would do even more if I knew that other people were using it.

On the whole I’m very satisfied with the galleries I produce using pix.js. I find them attractive, informative and easy to navigate; I seldom get the same pleasure from browsing other people’s photos. The main reason, I’m afraid, is that personal websites, and even club websites, are fading from existence. This is for several reasons. The various techniques available are more extensive and more complicated than they used to be; search engines bias their results in favour of the commercial sites which supply their revenue; and the lure of social media has induced users to surrender control to sites which inevitably let them down.

The most serious weakness in pix.js is the amount of work needed to prepare the images. I’m used to making an effort to tidy up the largest version, but rescaling to a sequence of smaller sizes is pure drudgery which becomes more onerous as time progresses: screens become simultaneously larger and smaller, and resolution becomes more variable, so that a larger range of sizes is needed.

The ideal solution, I think, would be to provide a web service which took a photolist and an incomplete set of images as input, and which automatically generated the missing images (by reduction). This would not be hard to write for someone familiar with CGI and with the process for installing C programs on web servers; but I have no knowledge of either topic and I’m a little sick of learning new interfaces. Perhaps one day...

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

In the days of cathode ray tubes, there were about 72 or 96 screen pixels per inch. Images had intrinsic sizes in pixels, so pixels were the natural units for their display. You would write:

<img width=288 height=288 src=... >

It was possible to use more natural units such as inches but the effect would not be as good. For instance if one had a 288x288 image and specified the HTML size as 4"x4", then the image would display perfectly on a screen with 72 pixels per inch, but on a screen with 96 pixels per inch each pixel in the image would cover 1⅓ screen pixels, leading to a loss of sharpness.

Then LED screens came in and 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 half the size the author had intended, or of reinterpreting pixels as implicit sizes of about a 72nd or 96th 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.

If you want to take advantage of hi-res screens, you need to supply a larger image, eg. 1200x800, and tell the browser that it can use it in place of the original if the screen resolution is sufficient. This can be done by means of the srcset attribute.

When I first owned a retina laptop I found that the improvement in image quality was conspicuous. pix.js was well positioned to take advantage of hi-res screens since the regular images have the same aspect ratio, allowing the larger ones to be offered as hi-res alternatives to the smaller ones. Regular images can be offered as hi-res alternatives to thumbnails except when the aspect ratios differ, in which cases, if you want to take advantage of hi-res screens, you need to provide a separate series of images (usually just a single extra image).

For a long time pix.js did just this, offering all sizes of an image to browsers to allow it to make the best choice for display (which may depend on screen resolution and internet bandwidth).

But then I got a bigger desktop computer, generated some larger jpgs, and found that browsers were choosing higher resolutions for large images than could be justified. My impression is that for most large images (say 1400×1000, which is one of my standard sizes), increasing the resolution brings minimal visual benefit (though there are certainly exceptions). It’s a question of focus. If you view a large image as a whole, the resolution isn’t very critical. If you focus on a small region, a sharper image looks better. Thumbnails and text automatically induce the eye to focus. (Resolution also affects texture: grass looks grassier at hi-res.)

There is a cost associated with the browser’s choice of an oversized image: it will take longer to download. After a couple of false starts I decided to take the decision out of the browser’s hands. I now offer just a single image, choosing the one whose size corresponds to the logical size of the region or one giving higher resolution according to a criterion of my own devising. It seeks a resolution between 1 and the screen’s device pixel ratio, preferring higher resolutions for smaller images, but allowing the user to choose between various resolution levels. The user can control the resolution globally through the gallery menu, or for individual images using [shift]↑ or [shift]↓, while the menu for individual images tells you what resolution (if ≠1) is being used.

This is why I stopped using srcsets. If you’re willing to let the browser do what it thinks best behind the scenes, then srcsets are a painless mechanism. If you then want to find what resolution the browser has chosen, you have to do more work than you would like; and if you want to constrain its choice, you find that you’d be better off making all the decisions yourself.

One limitation in what I do is that I take no account of internet bandwidth. I suspect that most browsers take no account of it either. A browser which has knowledge of the bandwidth can expose it to javascript through the experimental downlink interface. This is not currently supported by Firefox or Safari. Since generating an estimate is the only difficulty, it is reasonable to suppose that not all browsers perform this task. If and when they do, no doubt they will also support the javascript interface.

A few details on how I choose a resolution. A user parameter λ takes a value between 0 (minimum resolution) and 1 (maximum). A target resolution between 1 and the device pixel ratio is determined as a function of λ and the image size in megapixels. The image chosen for display is the one closest (geometrically) to the target.

The following table shows the target resolution when the minor image dimension is as given by the column header (the major dimension being 1·4× as great) and λ is as given by the row. The device pixel ratio is 2.

       350   500   700  1000  1400  2000
0.05  1.28  1.14  1.07  1.04  1.02  1.01
0.10  1.53  1.28  1.15  1.07  1.04  1.02
0.15  1.71  1.41  1.22  1.11  1.06  1.03
0.20  1.83  1.52  1.29  1.14  1.07  1.04
0.25  1.90  1.62  1.36  1.18  1.09  1.05
0.30  1.95  1.71  1.42  1.22  1.11  1.06
0.35  1.97  1.78  1.49  1.26  1.13  1.07
0.40  1.99  1.84  1.55  1.29  1.15  1.08
0.45  1.99  1.88  1.61  1.33  1.17  1.09
0.50  2.00  1.92  1.66  1.37  1.20  1.10
0.55  2.00  1.94  1.72  1.42  1.22  1.11
0.60  2.00  1.96  1.77  1.46  1.25  1.12
0.65  2.00  1.98  1.81  1.50  1.28  1.14
0.70  2.00  1.99  1.85  1.55  1.31  1.15
0.75  2.00  1.99  1.89  1.60  1.34  1.17
0.80  2.00  2.00  1.92  1.66  1.38  1.19
0.85  2.00  2.00  1.95  1.71  1.43  1.22
0.90  2.00  2.00  1.97  1.78  1.49  1.26
0.95  2.00  2.00  1.99  1.86  1.58  1.32

The bold rows illustrate the default resolution (0·2) and ‘high’ resolution (0·6).

Early in the 2000s I adopted 7×5 as my default aspect ratio. This was a compromise between the 3×2 used by 35 mil film and the 4×3 preferred by digital cameras. It’s also quite a good ratio, but it has the consequence that all my photographs need to be cropped in one way or another. If I was starting afresh I’d probably use 4×3.

For thumbnails 7×5 is not just my default but my uniform practice. (I don’t think it’s forced on me by pix.js – I could do anything I wanted.) The main drawback is that since some images are portrait and some are landscape, the gallery main page is rather higgledy-piggledy. Most photo gallery software shows the influence of graphic designers in imposing a smoother appearance. Of course this cannot be done without some violence to the individual images. Part of my reasoning is that thumbnails are not there purely for the main gallery page – they can be used in any way the user wants. In my case I display them on notes pages (which admittedly are even clumsier) and in routemaster.

I could, moving in the opposite direction, have decided that thumbnails would always preserve the aspect ratios of regular images. But some of my panoramas are very letter-boxy, and a thumbnail which was consistent with them would just be a thread on the page. Still, all options have some merits.

My scale factors increase by a factor of about sqrt(2) through the regular images, with the thumbnail being smaller than the smallest by a slightly larger factor. Since my minor regular dimension now ranges from 350 to 2000, there are a large number of scale factors (and a lot of resizing has to take place). The resizing could of course be automated.

I have often agonised over whether a more widely spaced set of scale factors would do as well. I always come to the conclusion that it wouldn’t. I’ve been confirmed in this by web searching, which turned up a commercial server-side library whose scale factors were quite similar to my own.

At present the set of physical display sizes is the same as the set of intrinsic pixel dimensions (although the pixel dimension used may not equal the physical display size). This restriction is unnecessary, and becoming outdated as screen resolution increases.

Two alternatives occur to me. The first is to allow a discrete set of display sizes intermediate between intrinsic pixel sizes (eg. to store images at widths 500, 1000 and 2000 but to be also willing to display them at 350, 700 or 1400). This would save on the number of images kept on disc, but the results would be worse – for both quality and bandwidth – than if I pregenerated images at the intermediate sizes.

The second alternative is to be willing to display an image at any physical size whatever, choosing a display size to fill the window and a resolution to match. In a way this simply takes the preceding alternative to its logical extreme, and it might be thought to have the same weaknesses to a greater degree. But in compensation it makes optimum use of the window size it’s given. Given that high definition screens are now the norm, reliance on intrinsic pixel sizes seems to be little more than a hangover. I’ll probably adopt this option one day. (I might then store images with minor dimensions 375, 600, 960, 1500, 2400.)

Prior to 2024 I used a rather clumsy scheme for returning from thumbnails. If an info page had a thumbnail link to an image, then I wanted the user to be able to navigate between images and finally return to the info page, either by clicking the ‘↵’ link or by hitting carriage return. I enabled this by allowing the photolist to contain the page and anchor to return to in ‘retpage’ and ‘retid’ fields. This was unsatisfactory in that thumbnails for the same image could only return to a single location, and it complicated the construction of photolists. The window.history object of the DOM can be used more slickly for the same purpose. This is what I now do, rendering all references to retpage and retid obsolete. Unfortunately I think there are bugs in Firefox’s history mechanism, but hopefully other users won’t notice them.

using pix.js : general reflections : srcsets : aspect ratios : thumbnails : scale factors : retpage/retid

xml : aliases : contents of the list : images : videos : dividing images into sections : categories : layouts : the size list : sundry variables : examples

The photo list is more complicated. The following is a simple example:

<photolist>
<title>Gran Canaria mountain biking photos</title>
<alias name="l" shape="[980,700]"/>
<alias name="p" shape="[700,980]"/>
<base imagedir="images"/>

<img name="swerve" title="Rocks near the Embalse de los Hornos" shape="p"/>
<img name="ridgepath" title="Ridge path" shape="p"/>
<img name="tracey" title="Tracey on the ridge, Roque Nublo behind" shape="p"/>
<img name="precipice" title="Colin on the ridge" shape="p" 
     caption="keeping well to the right"/>
<img name="artenara" title="Colin on the ridge" shape="p"/>

<sizes>
  <size suffix="@1" scale="10"/>
  <size suffix="@0" scale="5"/>
  <size suffix="@t" scale="2" type="thumb"/>
</sizes>

</photolist>

The photo list is an XML specification of the gallery properties. Aliases can be used to avoid repetition of lengthy parameters. Note that all URLs in the photo list are (logically) relative to the list itself (ie. to the XML file), not relative to some page which makes use of the photo list. You will avoid confusion if you keep the XML parameter file and the HTML stub in the same directory.

I say that URLs are ‘logically’ relative to the list because that is how I intend them to be interpreted. However since you may include HTML snippets, and these may embed URLs in various contexts, I cannot completely enforce consistent handling. If you try to be clever, use absolute URLs.

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 ‘@1’ will be specified as

suffix="@1"

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. Thus if ‘size’ has attributes ‘suffix’ and ‘scale’ we may write

<size suffix="@1" scale="10"/>

‘size’ is the tag name, ‘suffix’ and ‘scale’ the attribute names, and ‘@1’ and ‘10’ the values.

The elements governed by a tag comprise its attributes and other tags nested inside it. In the example we have given there are no nested tags so the tag is closed by a ‘>’ with a preceding ‘/’.

If a tag has tags nested inside it then it occurs in the form

<tag [attributes]>nested tags</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. Thus the ‘photolist’ tag in the example photo list above has various tags, such as ‘title’, ‘base’, ‘sizes’, etc. nested inside it; and ‘sizes’ has ‘size’ tags nested inside it; and so forth. There is no limit to the amount of nesting which may occur in an XML file.

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="https://www.routemaster.app/?track=https://www.masterlyinactivity.com/routemaster/routes/grancanaria"/>

(Note that the final solidus is not part of the URL but indicates that the tag has no elements. ‘uri’ is a posh name for URLs.)

Whenever an XML item has a URL 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 URL relative to u1, and u1 is the name of a URL 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="https://www.routemaster.app/?track=https://www.masterlyinactivity.com/routemaster/routes/grancanaria/artenara.tcx">

The photo list contains specifications of the images, usually divided into sections; it also contains the size list and sundry additional parameters. We look at these in turn.

Images are the main content of the XML list file. An image is specified by the tag ‘img’ with a number of attributes, listed exhaustively here. The function of some of the attributes can alternatively be performed by nested tags, but no other tags are nested inside an image.

The name is a short character string which you will use to identify a particular photo. If the name you choose is ‘swerve’ then the names of the associated image files will be (eg.) swerve@t.jpg (the thumbnail), swerve@0.jpg (the small version), and swerve@1.jpg (the larger version). These names are made up from the ‘name’, from the suffices of the various sizes, and from the ‘.jpg’ file extension.

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.

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

More generally, the title is not restricted to flat text and may be an HTML snippet. The XML notation required for this is rather clumsy. The following is an example of its use:

<img name="traceyatsalineras" shape="p" rawshape="rp">
    <title><![CDATA[Tracey <i>en corniche</i>at Salineras]]></title>
</img>

In order to supply an HTML title, it needs to be the text component of a ‘title’ element rather than an attribute of the ‘img’. The text begins with the sequence

<![CDATA[

which is followed by the HTML and concluded with the sequence

]]>

All this is included between the standard

<title>...</title>

Beware the use of relative URLs in the HTML snippet. When I get round to it I will convert relative link hrefs into absolute URLs interpreting them properly (ie. as relative to the XML config file), but other contexts in which URLs may occur (eg. image sources) will be left intact.

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

caption="This is harder than it looks!"

As before, the caption may be an HTML snippet eg:

<img name="traceyatsalineras" title="Tracey on the path below Salineras"
     shape="p" rawshape="rp">
   <caption><![CDATA[This is a <i>road</i> according to the map.]]></caption>
</img>

This field can be used to request a vertical divider before the display of a thumbnail in the image table. (The divider is omitted if the thumbnail occurs in the first column.) It also determines the visibility of the image. Legal values are

"|" "none" "*" "|none" "|*"

The ‘|’ requests the divider. An image whose visibility is ‘none’ will not be displayed in the table unless the page has the special mode ‘a’. An image whose visibility is ‘*’ will be given special treatment: the starred images will be used to present gallery highlights in routemaster and metagalleries. You can limit display to starred images by using the ‘*’ mode among the URL options.

If an image has an overlay, it must be produced for all sizes other than thumbnail (and hithumb), and has the same name as the corresponding image except for having the “.png” extension. (If there is no raw image, you don’t need a raw overlay.) You then specify it in the following format:

<img name="route" title="Route map" shape="[1008,714]"  
       thumbshape="thl" overlay="overlay"/>

The value of the attribute (in this case “overlay”) is a character string which will be used in generating the image caption (which in this case will say ‘Add overlay’/‘Remove overlay’).

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.

The shape is ‘heritable’ in a sense defined below.

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 the shape you supply is the size of the image whose suffix is the suffix of the first entry in the size list. You supply images for the other entries in the size list, and their sizes are related to the first one by the ratio of the scale factors in the list.

You may include a scale factor for raw images in the size list, thus implying that a raw image is available for every regular image, and that its size is related to the standard size. I never do this, but often provide raw images specifying their sizes individually in the list (using aliases). The format is again [w,h]. The raw shape is heritable. If the size list or the inherited property imply a raw size for an image which in fact has no raw version, you can override the implied size by the dummy field

rawshape=""

The shape of thumbnail images is usually inferred from the shape of main images. 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 again [w,h]. Every image needs a thumbnail, by whatever means you specify its size.

You are recommended to provide a size for hi-res thumbnails in your ‘sizes’ list (see below). If in a particular case the hi-res thumbnail does not have the scale factor specified in the ‘sizes’ field, you can provide it as a separate attribute. The format is sfx[scl], sfx being the suffix for the thumbnail and scl being the scale factor relating the hi-res to the normal thumbnail. The special value of an empty character string can be used to specify that a particular thumbnail has no hi-res equivalent.

You can also use this attribute to prevent pix.js from offering a regular image as a hi-res alternative. Sometimes the thumbnail won’t just be a reduction of the regular images, and you won’t want a regular image to be treated as a substitute for it.

Therefore I sometimes write:

<img name="crash" title="falling off" size="ql" thumbshape="qlth" hithumb="@h[2]"/>

telling pix.js that the image crash@h.jpg is a version of the thumbnail image whose linear size is twice that of the thumbnail (as specified through its thumbshape in this case, or through the size list in other cases).

High resolution thumbnails work by means of the HTML ‘srcset’ attribute, which allows any number of alternative resolutions. Accordingly pix.js allows you to provide any number of hithumbs, eg.

<img name="crash" title="falling off" size="ql" thumbshape="qlth" hithumb="@h[2] &k[5]"/>

Note that they are separated by spaces (not commas). So now I also have crash@k.jpg at 5x the linear size.

I seldom use this option (how many screens offer pixel ratios of 5?). The case in which I find it useful is when I want to offer magnified versions of a thumbnail rather than the more common reduced versions of the main image (example).

If the scales at which an image is available are listed in a size list which is not the default, then its name should be given against the ‘sizes’ attribute; eg.

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

which might be used if the ‘loshornos’ image had a different set of scales than most of the others.

This value is the name of the directory containing the image. It is relative to the XML config file. The filename of the image will be constructed as

imagedir + ‘/’ + name + suffix + ‘.’ + extension

This gives the extension used in constructing the filename. The default is “jpg” (note that there is no “.”). I use an “extn” attribute only for two purposes: for gifs and for videos.

These two values were formerly used to take you back to the html notes page reached by clicking on a thumbnail. I now achieve a similar effect using the window.history mechanism and now ignore any ‘retpage’ or ‘retid’ supplied.

This tells pix.js that an image belongs to a particular category. You may limit a gallery to pictures in a category through the URL options; you can also change the view through the menu icon or the ‘m‘ key.

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

<img name="traceyrocks" title="Tracey heading for the Dog’s Leap" 
     caption="looser than she would like" shape="p" display="|*" 
     thumbshape="[140,196]" cat="2014" imagedir="images"/>

A video is provided as an image with “mp4” as its extn. Whereas a still image is provided at a number of sizes corresponding to the associated list, and also at one or two thumbnail resolutions, a video is supplied as a single .mp4 file whose size you specify, together with a poster image of the same size, and one or two thumbnail resolutions. If there is a ‘hithumb’ entry in your size list, then you should provide a hi-res thumbnail at the appropriate scale relative to the main thumbnail (example).

The validation option does not check the size of the .mp4 file.

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.

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.

The section tag has a number of attributes. Firstly, all the heritable attributes of images are inherited from the equivalent attributes of their section (and inherited in turn by the section from the base of the list).

Secondly the section tag has a number of attributes of its own.

The section title.

This supplies a link to one or more GPS tracks associated with the section (separated by spaces if there is more than one). The URLs should be any linkable form of a GPS track. I’m used to routemaster, but you can link into Wikiloc or any other repository if you like.

If there is more than one GPS track, pix.js will attempt to show how the tracks are associated with thumbnail images for the section. If ‘|’ dividers correspond to the tracks, it can work out how to do this.

Suppose you have 8 thumbnail images in a section and two GPS tracks. Suppose that the first 2 images correspond to the first GPS track, then (after a ‘|’ divider) come another 3 images with no GPS track, and finally (after another ‘|’ divider) come the last 3 images corresponding to the second track. In order to enable pix.js to see the correspondence between thumbnails and tracks, you should provide ‘-’ as a dummy GPS track inbetween the two genuine ones. You may therefore write something like:

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

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

The caption for a section will be displayed after the title and before the thumbnails. As with image captions, it may be an HTML snippet supplied as a caption element of the section using the ‘CDATA’ notation. It may be limited by category.

This attribute may be used to supply an HTML anchor for linking to the section.

This specifies a format for tidily laying out the contents of the section: see below.

This is auto-generated for metagalleries and should’t be used otherwise.

The ‘cat’ attribute may be used to allow partial views of a gallery. You can view the images by category or all together, according to the URL option (see above) and subsequence adjustments from the menu icon.

Our Cape Verde photos are a good example. Sometimes we want to look at the pictures for our 2016 or our 2018 visit on their own, while at other times we want to see all pictures of a particular route (eg. Route 101, which we rode on both visits).

It is not a good idea to use categories purely in order to allow a single gallery to do the job of more than one.

Layouts may be used if you want a gallery to present images for direct viewing (rather than as thumbnail links) in an attractive way. Our doors page is an example. The options may be expanded in future.

A layout must be specified as an attribute of a section, thus:

<section layout="4:40%:n">

It contains 3 fields separated by colons which may occur in any order.

The number of columns must divide the number of images displayed in the section. Unless you use one of the fancy layouts described below, the display will normally look best if all images in a row have the same height and all images in a column have the same width; but these conditions are not enforced, and sometimes breaching them gives a good effect (as with our Kangchenjunga trek).

The size indicator gives the recommended percentage of window height to be occupied by the tallest image in the first row. The available size will be chosen which most closely matches the recommendation. The default is based on the number of rows in the layout.

The flags may contain the letter ‘n’ indicating that image titles should never be shown in the gallery page, and the letter ‘t’ indicating that the display should always use thumbnail images.

The normal behaviour of pix.js is to display small regular images (it is often useful to provide especially small versions for this purpose). Thumbnails will be displayed instead if they have the same shapes and if there isn’t enough room for regular images.

A limited number of fancy layouts are implemented. They will be chosen automatically if your image shapes permit them.

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="@1"

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 ‘@1’, then the filename will be swerve@1.jpg. This may be modified by a directory path.

My practice is to reserve the empty suffix for the raw size, and to use non-empty suffices for the various non-raw sizes. You are urged not to use the empty suffix for non-raw sizes.

It is advisable to begin suffices with a punctuation character so that they look right to the eye. Mine always begin with a ‘@’. The suffices are part of the image URLs, and some punctuation characters have special meanings within URLs; on the whole you want to avoid these characters. However some only have special meanings in certain contexts, and are safe in others (I think ‘@’ belongs to this class). So choose with care. I believe that hyphen, underscore, tilde and full stop are always safe. I don’t think you will have any problems with ‘@’, ‘:’, ‘+’, ‘$’ and ‘,’. You will probably get away with several others.

This is a numerical value giving the relative sizes of images with the given suffix. The only circumstances in which it may be omitted are for the suffices of types ‘raw’, ‘thumb’ and ‘hithumb’ if the relevant scalings are given separately for each image (possibly taking advantage of inheritance).

The meaning of the scale factor is as follows. The images of various sizes are assumed to have the same aspect ratio, and to have their dimensions proportional to the scale factors in the size list. The ‘shape’ assigned to each image gives the dimensions corresponding to the first size in the list. The dimensions of other sizes are scaled in proportion to the scale factors. So if an image has shape [1000,700], and the first size in the list has scale factor 18 and suffix “@3” while the second has scale factor 9 and suffix “@2”, then the jpg whose name ends “@3” will be of size [1000,700] and the jpg whose name ends “@2” will be of size [500,350].

The regular images – those which are not raws, thumbnails or high-res thumbnails – must have the same aspect ratio as each other and sizes in proportion to their scale factors. The first item in the sizes list must be a regular size. But for the special images – raws, thumbs, and hithumbs – it is possible to override the shape for an individual image through its attributes in the list. In the case of raws and hithumbs, this includes being able to say that they are not available for a particular image, even though they have an entry in the sizes list.

The scale factor for the hi-res thumbnail has a completely different meaning from the other scale factors. The other scale factors give the relative dimensions of the jpgs to each other: only their ratios are significant. The scale factor of the hi-res thumbnail is its dimension relative to the normal thumbnail. It may be fractional, and you can write it with a decimal point.

The reason the hithumb scale factor is treated differently is this. A hi-res thumbnail is not needed except when the regular thumbnail is a different shape than the sizes list implies. Therefore the cases in which a hithumb scale factor is needed are those in which the normal definition of scale factors cannot be used.

This is a character string specifying a type corresponding to the image size. For regular 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:

And you are recommended to provide a further entry in the size list ‘hithumb’ for high-resolution thumbnails. When an image is displayed at a particular size, higher resolution versions at larger sizes (but the same aspect ratio) are offered to the browser as alternatives for high-definition screens. A large regular image may be offered as an alternative to a small one. If thumbnails have the same aspect ratio as regular images, and the image is not a video, then small regular images will be offered; there is no point in providing a hi-res thumbnail in these cases. But if the thumbnail has a different aspect ratio from the regular images, or the image is a video, then a higher resolution alternative will not be available unless it has been created for the purpose.

So, for those images whose thumbnails do not have the same aspect ratio as the regular images, or which are videos, you are urged to produce hi-res alternative thumbnails whose properties (suffix and scale factor relative to the normal thumbnail) you specify in the sizes list. A suitable scale factor is 2, 2.5 or 3. If all your thumbnails have the same aspect ratio as the regular images, and none are videos, there is no need to pay any thought to thumbnails.

An entry in the sizes list will look something like the following:

    <size suffix="@h" scale="2" type="hithumb"/>

You may provide more than one hithumb size if you want. This is not usually worth doing, but see above for an exception. Thus you may end up writing

    <size suffix="@h" scale="2" type="hithumb"/>     
    <size suffix="@k" scale="5" type="hithumb"/>

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

fontsize="12"

I have never used this myself, and rather regret providing it.

Prior to 2024 only a single size list could be provided; therefore all images had to be available at consistent sets of scales. You may now provide as many lists as you want. Only one can be provided with no name; all the others need a ‘name’ attribute, eg.

<sizes name="small">
  <size .../>
  <size .../>
</sizes>

The heritable ‘sizes’ attribute of images can be used to give the name of a non-default size list. Thus you may tell pix.js to use the ‘small’ set of sizes for all images by writing

<base imagedir="images" sizes="small" ... >

or you can specify the small set for a particular section

<section title="More rides from Valsequillo" sizes="small">

or you can say that a particular image comes from the small set of sizes by writing

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

You need to be careful if trying to use multiple size lists within a layout. When displaying the gallery, pix.js has to choose a single entry from the size list which fits the screen; if there is more than one size list, the notion of a single entry becomes tenuous. However it will make the right choice if one exists. You should be safe in the normal use of size lists, which is for some lists to be truncated versions of others to allow for there being no images above a certain size.

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 multiple titles with categories to distinguish them, e.g.

<title>Gran Canaria cycling photos</title>
<title cat="2012">Gran Canaria cycling photos (2012)</title>
<title cat="2015">Gran Canaria cycling photos (2015)</title>

See above.

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="..."/>

You may specify a category if you wish.

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 gallery.

The attributes of a link are as follows.

A full example may look like this:

<link name="into" href="intro17.html" cat="2017" display="|" type="origin"/>

Some text to put at the top of the page. As with image and section captions, it may be an HTML snippet.

Images inherit heritable properties from sections, and sections inherit heritable properties from the ‘base’ tag. This enables you to minimise the amount of repetition. An example is the image directory. This will usually be the same for all images, and you can therefore specify its location once as the ‘imagedir’ attribute of the ‘base’ tag. Eg.

<base imagedir="canarypix" shape="l"/>

The image directory will then be inherited by all images, and does not need to be specified further except for any images not in ‘canarypix’. In the same way, having made ‘l’ the default shape, you have no need to specify a shape whenever the default applies. You can override the default for individual sections or images.

It is unlikely that the category will be inherited from the base, but common to have all images in a section belong to the same category, so there will be no base category, a category for most sections, and categories for images only when the section does not correctly assign a category to them.

The XML photolist and the HTML stub perform a single function split across two files. The HTML page is told the name of the XML file through the pix argument, but other pages loading the XML file (eg. routemaster) may like to know the URL of the HTML file so that they can link to it. This is done through the ‘gallery’ tag, eg.

<gallery href="pix.html"/>

This tag allows you to control colours (other than link colours) for your gallery (example). (For link colours, see below.) Its attributes can be any interpretable HTML colour (eg. ‘black’, ‘#ff0000’), except for ‘farg’ (described below), and fonts may be any legal CSS font specifications. “cat” may be added as an attribute. (Example.)

Note that if you specify a style for a category, and the user moves from viewing that category to another via the menu icon, then the style will not change even if the new category has a style associated with it. I could fix this if I really considered it a bug.

farg: this may be either a single legal CSS background specification (eg. ‘#ff0000’ or ‘url(images/carpet.gif)’) or a list of background images at different resolutions, eg.

url(images/carpet.gif) 1x url(images/carpet2.gif) 2x

The components are separated by spaces. The ‘url’ flag is not needed, so you can more tersely write

images/carpet.gif 1x images/carpet2.gif 2x

(but remember that the ‘url’ is needed if only one item is present). The farg value is itself enclosed in quotation marks as part of the notation for XML attributes.

The following tags have been accepted in the past but are being phased out. They now produce a warning message. Note that this does not affect their use as attributes, in which capacity they remain valid, except for ‘pixpage’ which is now superseded by ‘gallery’.

  imagedir  origin  hithumb    thumbshape pixpage

I used to provide an example of a fairly full photo list on this page, but it was forever going out of date and the number of features calling for illustration grew beyond anything that can be covered by a single example. Here are links to a few of my own photo lists. If you view them in a browser it may impose its own formatting, but if you save them to disc you can view them as normal text files.

xml : aliases : contents of the list : images : dividing images into sections : categories : layouts : the size list : sundry variables : examples

changing link colours : validation : metagalleries : thumbnail links : technical details : random thumbnails : duplicate image names : multiple photo lists : using pixlib separately : what is a srcset?

If you use the style tag to change a gallery’s colour scheme, you will want to change the link colours at the same time. This cannot be done in the same way, and depends on a rather clumsy mechanism. (Unfortunately there is no interface between Javascript and CSS link pseudoclasses, so I’m forced into convoluted measures.)

What you need to do is to insert a single Javascript instruction in your HTML stub before the invocation of pix.js. The format is as follows:

<script>var pixcols = { link:"#083090" ,  visited:"#630000" ,    active:"#404040" , 
                        mlink:"#083090" , mvisited:"#630000" , mactive:"#404040" }</script>

where the values you provide are colours for links/visited links/active links, first for the main display, then for menus. Everything you don’t specify will be left to default. They can be any HTML colours: ‘darkgoldenrod’ is fine.

Your HTML page will therefore end up looking like this:

<html><head>
<meta charset=utf-8> 
<script>var pixcols = { link:"#083090" ,    visited:"#630000" ,     active:"#404040" , 
                        mlink:"#083090" , mvisited:"#630000" , mactive:"#404040" } </script>
<script src="../pix.js"></script>
</head><body></body>
<script>pix("senales.xml")</script>
</html>

If I can find a better way of doing this, I’ll change how it works. Note that you cannot limit link colours to a particular category.

It is easy to make errors in generating the images and the list file. pix.js has a validation option, invoked by specifying a ‘v’ in the mode argument to the picture page, i.e.

https://www.masterlyinactivity.com/kumaon/pix.html?mode=v

When the picture page is invoked in this way, instead of generating a table of thumbnails it generates a list of all images and sizes implied by the list file. The images are fetched in turn. When they are loaded, if their physical size is found to agree with the size attributed by the list, the corresponding entry is deleted. If a discrepancy is found, a note is made in the list. When it has all settled down, no images should be left in the list except those whose sizes are given incorrectly and those which have not been found.

This is quite a slick procedure if you run it on your desktop image of a gallery. On the internet it may take several minutes to complete if your images are large.

A metagallery is a gallery of galleries – the thumbnail links take you to galleries rather than images. Our touring gallery is an example. It contains sundry XML tags the same as for regular galleries and images divided into sections in a special way. There is no size list. You do not produce the XML for the images yourself: instead you go a gallery and view it with a ‘g’ in its mode, eg.

https://www.masterlyinactivity.com/kumaon/pix.html?mode=g

This gives you the code to copy and paste to your XML file.

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 (or the showimg() 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.

Alternatively you may write

<script>showimg("swerve",500)></script>

Instead of displaying your thumbnail, it displays your main image, scaled to width 500.

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

<script src="https://www.masterlyinactivity.com/pixlib.js"></script>
<script>loadpix("list.xml")</script>

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

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

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

which may occur in the header or in a style sheet.

A neat trick occurred to me recently, which is to use a style like the following:

<style>img.pix{width:140px;height:140px;object-fit:cover;border:1px solid;padding:2px}</style>

This shows the thumbnails as cropped to square, even if there true shape is oblong. Our 2022 Gran Canaria notes illustrate the result.

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>

or

<script>showimg("swerve",500,1)></script>

Notice that the first image is invoked by supplying ‘0’ as an argument, the second by supplying ‘1’, and so forth.

It is also possible to specify a link for the thumbnail other than the usual full-size picture: see below.

The operation of thumb()/showimg() is more complicated than you might expect, since javascript I/O is asynchronous and when you call these functions the result of loadpix() may or may not have been delivered.

In the former case thumb() is able to generate the HTML text exactly as just described, but in the latter case it merely generates a stub, subsequently modifying the DOM through an event handler invoked when the load has completed.

You may sometimes want to show a random thumbnail for a gallery or metagallery. We do this on our home page. You call thumb() omitting the name or get a similar effect with showimg.

The result is as follows. If there are starred images in the photolist, you get a random choice from them. Otherwise you get a random choice from images whose ‘display’ attribute is not ‘none’.

The thumbnail will link to the gallery, not to the individual image.

If you supply a null name and a random number x as the serial number (0≤x<1), then the random choice will be detertmined by the random number you supplied rather than being generated for the purpose. This allows you to use daily changing random numbers rather than fresh numbers on each call; or to ensure that if you regenerate an image you get the same as you started with.

If you combine photos from several image 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 showimg() 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.]

Note that routemaster provides no mechanism for choosing between images with the same name in a list.

If you are showing just one random thumbnail from a list, you may want to show a random thumbnail from another list too. To support this, thumb() and showimg() have an additional optional argument (preceding the name, which is of course omitted for random images) which gives the number of the list to use.

So I load photo lists into our home page by the 4 lines

loadpix("galleries/touring.xml") ;
loadpix("galleries/mtb.xml") ;
loadpix("galleries/treks.xml") ;
loadpix("galleries/misc.xml") ;

and then write, eg., <script>thumb(2)</script> to get a random thumbnail from the trekking gallery (the galleries are numbered 0, 1, 2... in order of loading). I could make loadpix return the associated number if I thought there was a risk of confusion.

The full calling sequences for thumb() and showimg() are as follows:

thumb()

which generates a random thumbnail link for the gallery loaded;

thumb(listno)

which generates a random thumbnail link gallery number listno;

thumb(name)

which generates a thumbnail link for the named image;

thumb(name,serialno)

which generates a thumbnail link for the serialnoth image of the given name; and

thumb(obj)

which generates a thumbnail link according to the fields of the supplied object argument.

showimg(name,width)

which generates the named image reduced by the given factor (use a null image name to get a random image);

showimg(name,width,serialno)

which generates the serialnoth image of the given name reduced by the given factor; and

showimg(obj)

which generates an image according to the fields of the supplied object argument.

When an object is supplied as a parameter, its fields are as follows:

The link target for a normal call is set up in the link’s href field. A copy is put in its non-standard imglink field. But if you specify a different link location through the linkuri field of an object argument, the location which would otherwise have been used as a link target is nonetheless supplied in the imglink field. This allows you to associate a link to the image with the thumbnail whose own link to it has been overridden by something else of your own devising.

pixlib.js contains a javascript library for displaying images. It does most of the work for pix.js but can also be used separately (and is so used by routemaster). I previously sought to document it, but I kept on changing it in ways which made the documentation obsolete and didn’t believe it had any other users so deleted the documentation.

The software for displaying galleries as tables of thumbnails is part of pix.js but is also capable of more general use. (For a while it was included in pixlib.js, but since routemaster loads pixlib.js and has no need to display galleries, I removed it to cut down on load time).

The general outline of pixlib.js should be fairly clear, but if anyone wants to use it outside pix.js they should let me know to caution me against introducing changes which invalidate their software.

changing link colours : validation : metagalleries : thumbnail links : random thumbnails : duplicate image names : multiple photo lists : using pixlib separately : what is a srcset?

Before I wrote pix.js I had got into the habit – starting around 2001 – of 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 extensive. 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.

Added the option of multiple GPS tracks per section.

Added the categories feature and the ability to request display of hidden images. Started phasing out the javascript list format.

Removed the javascript list format. Added multiple headers. Ceased automatically adding ‘notes’ link. Used divs rather than a table for nav links.

Added overlays. Don’t generate captionbox unless the caption is too long for its position. Add ‘id’ for sections. Better info.

Added display="*" for images and cat="" for links.

Debugged the feature allowing links to be category-specific. Added newtabdiv() (previously in routemasterui).

Made GPS tracks open in a new tab and ensured cursor was a hand over thumbnails.

Added the menu button and the showimg() function. Added layouts.

At the same time I found that pix.js didn’t work correctly in the (rather old (2014)) version of Safari installed on one of my desktops. Mostly it was Safari’s fault (not handling Object.assign correctly), but there is also uncertainty about the semantics of history popstates. At any rate I hastily replaced Object.assigns of list entries by normal assignments (i.e. by reference), adding an extra field for the display value so that I don’t modify it in place. There is a risk of unanticipated consequences.

I validated pix.js in Firefox, Chrome, Opera and Safari. I should keep an eye on its performance in Safari, which is the least forgiving of the browsers available to me.

This was a thorough revision. I extended the layouts, added metagalleries and random images, tidied up some of the uglier code, and made the colour scheme customisable. I don’t have any further major changes in mind. If there’s something you think is missing, let me know.

I added “cat” as an attribute of styles.

I revised the calling sequences for thumb()/setimg(), which were getting out of hand, and added the ability to override the link target. I fixed a bug in the handling of retpage and later added z-indexes to avoid obliterating the nav icons with a layer of background colour.

A 1-line bugfix adding a z-index of 99 to the menu.

A 1-line mod allowing http images to be fetched from https via the routemaster.app fileserver.

The first of a series of changes to handle videos.

Allow any number of highthumbs.

An extensive set of changes. I added the ability to specify multiple size lists, and removed retpage and retid. I stopped using srcsets and added resolution controls and info to the menus. I replaced the ‘reduction’ argument to showimg() by ‘width’ (non-backwards-compatibly).

Later in the month I added the ‘image-set’ option to ‘farg’.

I added a further resolution control (with a fading-out message). To keep the complexity within reasonable limits I removed the thumbnail from the set of sizes available when a regular image is displayed.

I suspect that I still choose hi-res images more often than is justified.

• clone     • pixresize     • getncol     • linkp     • scrollitem     • redisplay     • display     • clickfactory     • function     • navigate     • quitimg     • retabulate     • galname     • tabulate     • setspacing     • validate     • textshape     • responsefactory     • optionparse     • pix     • render     • simulate     • tabinfodiv     • toolbox     • catclickfactory     • catbox     • gallerise     • hi     • gentable     • hoverfactory     • unhoverfactory     • addrow     • hr     • loadfactory     • hovercraft     • unhovercraft     • dolayouts     • matches

// www.masterlyinactivity.com/software/pix.html
var body,photolist,query,thispage,thisid,here ; 
// var mic = 'file:///Users/colinchampion/Desktop/masterlyinactivity/' ;
var mic = 'https://www.masterlyinactivity.com/' ;
var cols = { link: '#66aaaa' , visited: '#cc3388' , active: '#404040' ,
             mlink:'#2244cc' , mvisited:'#cc3388' , mactive:'#808080'} ; 
var style = { fg:'silver' , bg:'black' ,  mg: '#a4a4a4' , 
              mfg:'black' , mbg:'white' , farg:null , 
              font:'helvetica' , titlefont:'helvetica' , 
              title:null   , titlebg:null } ; 

var infowords = { exit: 'Exit full screen [esc key]' , 
                  enter: 'Enter full screen [f key]' ,
                  notes: 'notes' , origin: 'gallery' } ; 

if((typeof pixlib)=="undefined") document.write('<scri' + 'pt src="' +
    mic + 'pixlib.js"></scri' + 'pt>') ;

function clone(x,y) 
{ var i,z={} ; 
  if(x) for(i in x) z[i] = x[i] ; 
  if(y) for(i in y) z[i] = y[i] ; 
  return z ; 
}
var defstyle = clone(cols,style) ; 

// is link really 66aaaa or 4a8888 ?
if((typeof pixcols)!="undefined"&&pixcols)
  for(var field in cols) if(pixcols[field]) cols[field] = pixcols[field] ; 

document.write( 
    '<style>a:link{color:'+cols.link+';text-decoration:none}' + 
           'a:visited{color:'+cols.visited+';text-decoration:none}' + 
           'a:active{color:'+cols.active+';text-decoration:none}' +
           'a.m:link{color:'+cols.mlink+';text-decoration:none}' + 
           'a.m:visited{color:'+cols.mvisited+';text-decoration:none}' + 
           'a.m:active{color:'+cols.mactive+';text-decoration:none}</style>' ) ;

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

// 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()
{ var i,match=1,ncol,hcol=here.ncol ;
  // if it's an image, then call resize
  if(here.name!=null) { genpic('resize') ; return ; }

  // if the width is unchanged and there are no layouts, just return
  if(window.innerWidth!=hcol.wwid||hcol.laysize.length!=0)
  { ncol = getncol() ; 
    if( ncol.ncol!=hcol.ncol || ncol.laysize.length!=hcol.laysize.length ) 
      match = 0 ; 
    for(i=0;i<ncol.laysize.length&&match;i++)
      if(ncol.laysize[i]!=hcol.laysize[i]) match = 0 ; 
  }
  if(match) tabulate('resize',hcol) ;
  else tabulate(scrollitem(photolist.sect),ncol) ;
}
/* ----- getncol finds the number of table columns which fit the screen ----- */

function getncol()
{ var ncol,i,j,k,ind,sect=photolist.sect,maxw,w,h,kw,sizes ; 
  var sumw,maxh,ss,ll,err,errw,lay,item ;
  var maxcol = Math.floor((window.innerWidth-17)/(photolist.maxthumb+19)) ; 
  if(maxcol<1) maxcol = 1 ;  

  for(ss=[],ncol=i=0;i<sect.length;i++) 
    if(ll=sect[i].list,lay=sect[i].layout)
  { // 17 pix for scrollbar, 19 pix for margin, border...
    sizes = lay.sizes ;
    maxw = window.innerWidth - 19*lay.ncol - 17 ; 
    if(lay.mustthumb)
    { if(lay.sumwt>maxw) 
      { ss.push(null) ; // special value means ignore layout
        if((k=Math.ceil(ll.length/Math.ceil(ll.length/maxcol)))>ncol) ncol = k ;
      }
      else ss.push(-1) ; 
      continue ; 
    }

    for(k=null,j=-1;j<sizes.length;j++) 
      if((j>=0&&!sizes[j].type)||(j<0&&lay.canthumb)) 
    { if(j>=0) 
      { w = lay.sumw * sizes[j].scale / sizes[0].scale ;
        h = lay.maxh * sizes[j].scale / sizes[0].scale ;
      }
      else { w = lay.sumwt ; h = lay.maxht ; } 
      err = h/window.innerHeight - lay.ht ; 
      if(err<0) err = -err ; 
      if( k==null               // no previous scale found
       || (w<kw&&kw>maxw)       // this is < prev scale found which was illegal
       || (err<kerr&&w<=maxw) ) // this is legal and better than previous scale
      { k = j ; kw = w ; kerr = err ; }
    }
    ss.push(k) ;
  }
    else if(sect[i].href)
  { if(ll.length>ncol) ncol = Math.min(ll.length,maxcol) ; }
    else if((k=Math.ceil(ll.length/Math.ceil(ll.length/maxcol)))>ncol) 
      ncol = k ; 

  return { ncol:ncol , laysize:ss , wwid:window.innerWidth } ;
}
/* -------------------------------------------------------------------------- */
/*page*/
/* -------------------- create links line of the table page ----------------- */

function linkp()
{ var p,a,i,links=photolist.links,flag,s,ss,origin=null,style=photolist.style ; 

  if(!links||!links.length) return null ;  
  p = domcreate("p",null,'style',"text-align:center;font-size:100%;margin:6px"); 

  for(flag=i=0;i<links.length;i++)
    if( links[i].cat==undefined || links[i].cat==null || links[i].cat==query.cat
     || (links[i].cat==''&&!query.cat) )
  { if(links[i].display=='|') s = ' | ' ; else s = ' : ' ;
    ss = null ; 
    if(links[i].href)
    { a = domcreate('a',null,'href',links[i].href) ; 
      if(!origin&&links[i].type=='origin')
      { origin = links[i].href ; ss = " ↵" ; }
    }
    else  a = domcreate('span',null,'style',"color:"+style.mg) ; 
    domadd(a,links[i].name) ;
    if(flag) domadd(p,s) ; 
    domadd(p,a) ; 
    if(ss) domadd(p,domcreate('span',ss,'style',"font-size:90%")) ; 
    flag = 1 ; 
  }
  return [ p , origin ] ;
}
/* -------------------------------------------------------------------------- */

function scrollitem(sect)
{ var i ; 
  for(i=sect.length-1;i>0;i--) if(sect[i].scrollelement)
    if(sect[i].scrollelement.getBoundingClientRect().top<10) break ;
  return i ;
}
/* -------------------------------------------------------------------------- */

function redisplay(si,dir)
{ var sect=photolist.sect , sno=si[0] , ind=si[1] ,  item=sect[sno].list[ind] ; 
  var thisuri = thispage + '?image=' + item.name ;
  if(item.serialno>0) thisuri += '&serialno=' + item.serialno ;
  if(query.cat) thisuri += '&cat=' + query.cat ; 
  if(query.mode.length>0) thisuri += '&mode=' + query.mode ;
  if(here.ind==null) history.replaceState({ind:null,pos:scrollitem(sect)},'') ;
  history.pushState({ind:si},'',thisuri) ; 
  display(si,dir) ;
}
/* -------------------------------------------------------------------------- */

function display(si,dir)
{ var title,i,k,fetchitem,s,sect=photolist.sect ;
  var sno=si[0],ind=si[1],item=sect[sno].list[ind],retname,div ;
  var name=item.name,serialno=item.serialno,laction=null,litem=null ;
  var raction=null,ritem=null,title=sect[sno].title,retaction ;
  genmenu('del') ; 

  if(name!=here.name||serialno!=here.serialno) 
  { if(item.title) setdomtitle(item.title) ; 
    else if(photolist.title) setdomtitle(getcatval(photolist.title,query.cat)) ; 
  }
  here = { name:name , serialno:serialno , ind:si , ncol:null } ;

  if(query.mode.indexOf('n')<0) infoflag = 0 ; else infoflag = 1 ; 

  if(!dir)
  { if(sno=sect.length-1&&ind==sect[sno].list.length-1) dir = -1 ; 
    else dir = 1 ; 
  }
  function clickfactory(si,dirn) { return function() {redisplay(si,dirn);} ; }

  if( ( si = advance(photolist,here.ind,-1) ) ) 
  { laction = clickfactory(si,-1) ; litem = sect[si[0]].list[si[1]] ; }
  if( ( si = advance(photolist,here.ind,1) ) ) 
  { raction = clickfactory(si,1)  ; ritem = sect[si[0]].list[si[1]] ; }

  genpic(body,item,title,laction,litem,quitimg,raction,ritem,infowords,
         pixhelpdiv(photolist.style),dir,infoflag,photolist.style) ; 
}
/* ------------------------ navigate using the arrow keys ------------------- */

function navigate(e) 
{ var si,sect=photolist.sect ;
  if(e.keyCode==13&&genmenu('status')) { genmenu('del') ; return ; }

  if(e.keyCode==37)               // left arrow key
  { e.preventDefault() ; 
    si = advance(photolist,here.ind,-1) ; 
    if(si) redisplay(si,-1) ; 
  }
  else if(e.keyCode==39) // right arrow key
  { e.preventDefault() ;   
    si = advance(photolist,here.ind,1) ; 
    if(si) redisplay(si,1) ;
  }
  else if(e.keyCode==13&&here.ind) quitimg() ;
  else if(e.keyCode==13&&photolist.origin)  
    location.href = photolist.origin ; // return
  else if(e.keyCode==40&&!here.ncol) 
  { e.preventDefault() ; 
    if(e.shiftKey) genpic('lo-res') ; else genpic('reduce') ; 
  }
  else if(e.keyCode==38&&!here.ncol) 
  { e.preventDefault() ; 
    if(e.shiftKey) genpic('hi-res') ; else genpic('enlarge') ; 
  }
  else if(e.keyCode==32&&!here.ncol) 
  { if(genpic('spacebar')) e.preventDefault() ; } 
  else if(e.keyCode==70&&queryfullscreen()==0) // 'f' (full screen)
  { e.preventDefault() ; enterfullscreen() ; }
  else if(e.keyCode==73||e.keyCode==77) genmenu('toggle') ; // 'i'/'m'
}
function quitimg() 
{ if(query.mode.indexOf('n')<0) retabulate() ;
  else history.go(render.historylength-history.length-1) ; 
}
/* -------------------------------------------------------------------------- */
/*page*/
/* -------------------------------------------------------------------------- */

function retabulate() 
{ var sno = here.ind?here.ind[0]:-1 ; tabulate(sno) ; }

function galname(q)
{ var pixpage = thispage , doall ; 
  if(q.mode.indexOf('a')>=0) doall = 'a' ; 
  else if(q.mode.indexOf('*')>=0) doall = '*' ; 
  else if(q.mode.indexOf('h')>=0) doall = 'h' ; 
  else doall = null ;
  if(q.cat) 
  { pixpage += '?cat=' + q.cat ; if(doall) pixpage += '&mode=' + doall ; }
  else if(doall) pixpage += '?mode=' + doall ; 
  return pixpage ; 
}
function tabulate(sectno,ncol)
{ var i,sect=photolist.sect,caption,pfx,sfx=null,style=photolist.style,el ;
  var farging = style && style.farg ; 

  function setspacing()
  { var sparew,spareh,padw,padh,marh ;
    sparew = Math.floor((window.innerWidth-tabulate.tabw)/2) ;
    if(sparew<=80) padw = Math.min(8,Math.floor(sparew/2)) ; 
    else padw = sparew - 72 ; 
    spareh = Math.floor((window.innerHeight-tabulate.tabh)/2) ;
    if(spareh>8+sparew-padw)
    { padh = spareh - (sparew-padw) ; marh = spareh - padh ; } 
    else if(spareh>0) 
    { marh = spareh ; padh = Math.min(8,Math.floor(spareh/2)) ; marh -= padh ; }
    else 
    { marh = sparew - padw - 1 ; // subtract 9 as half of scrollbar, then add 8
      // if the table ht only slightly exceeds the page ht, avoid shunting the 
      // table so much further down that the user is forced to scroll unnecessar
      if(marh<0) marh = 0 ; else if(marh>-spareh/2) marh = -spareh/2 ;
      padh = Math.min(8,Math.floor(marh/2)) ; 
      marh -= padh ; 
    } 
    tabulate.table.setAttribute('style','margin:'+marh+'px auto;padding:'+
          padh+'px '+padw+'px;background:'+style.bg) ; 
  }

  if(sectno=='resize') 
  { if(farging) setspacing() ; here.ncol.wwid = window.innerWidth ; return ; }

  genmenu('del') ; 
  genpic() ; 

  setdomtitle(photolist.pagetitle) ; 
  if(sectno==-1) item = null ; 
  else history.pushState({ind:null},'',galname(query)) ; 

  if(query&&query.mode&&(i=query.mode.indexOf('n'))>=0) 
    query.mode = query.mode.substring(0,i) + query.mode.substring(i+1) ; 
  while(body.firstChild) body.removeChild(body.firstChild) ;

  if(!ncol) ncol = getncol() ;
  here = { name:null , serialno:null , ind:null , ncol:ncol } ;

  pfx = document.createElement("div") ; 
  p = document.createElement("p") ; 
  p.setAttribute('style',"text-align:center;font-size:140%;padding:5px 0 6px;" +
                         'margin:0;color:'+style.title+';font-family:' +
                         style.titlefont+';background:' +style.titlebg) ;  
  p.appendChild(document.createTextNode(photolist.pagetitle)) ; 
  pfx.appendChild(p) ; 

  if(p=linkp()) { pfx.appendChild(p[0]) ; if(p[1]) photolist.origin = p[1] ; }
  if(p=linkp()) 
  { sfx = document.createElement("div") ; sfx.appendChild(p[0]) ; }
  caption = getcatval(photolist.caption,query.cat) ; 

  tabulate.table = gentable(pfx,sfx,caption,sect,ncol,redisplay,style) ; 
  tabulate.table.setAttribute('style','margin:0 auto;padding:0;background:'+
                                      style.bg) ; 
  body.appendChild(tabulate.table) ; 
  tabulate.tabw = tabulate.table.offsetWidth ; 
  tabulate.tabh = tabulate.table.offsetHeight ; 
  if(farging) setspacing() ; 

  genmenu(body,function(){return tabinfodiv(photolist,query);},style) ;
  if(sectno>0&&sect[sectno].scrollelement)
  { i = sect[sectno].scrollelement.getBoundingClientRect().top ;
    window.scrollTo(0,i+body.scrollTop) ; 
  }
  else if(thisid)
  { el = document.getElementById(thisid) ; if(el) el.scrollIntoView() ; }
}
/* -------------------------------------------------------------------------- */

function validate()
{ var sect=photolist.sect , sizes , sty = photolist.style ; 
  var jpglink,ind,p,a,item,sfx,shape,span,img,png,sno,i,j,k,ll,start,len ; 

  function textshape(x,alias)
  { var i,shape = [ Math.floor(0.5+x[0]) , Math.floor(0.5+x[1]) ] ;
    for(i=0;i<alias.length;i++) if(alias[i].shape)
      if(alias[i].shape[0]==shape[0]&&alias[i].shape[1]==shape[1])
        return alias[i].name ; 
    return '[' + shape[0] + ',' + shape[1] + ']' ; 
  }

  function responsefactory(item,sizeno,nsizes,png,len) 
  { return function() 
    { var resp = item.resp , start = item.extn=='.mp4'?nsizes-1:0 , okay;
      var node , i , txt , flag , shape = resp[sizeno-start+1].shape ; 

      if(png) node = resp[sizeno+1].ovel ; 
      else node = resp[sizeno-start+1].el ;

      okay = (shape[0]==this.naturalWidth&&shape[1]==this.naturalHeight) ;
      if(!okay&&sizeno>nsizes) // allow some latitude in hithumbs
        if( Math.abs(shape[0]-this.naturalWidth) <0.1*item.thumbshape[0]
         && Math.abs(shape[1]-this.naturalHeight)<0.1*item.thumbshape[1] ) 
        okay = 1 ;

      if(okay)
      { node.parentNode.removeChild(node) ; 
        if(png) resp[sizeno+1].ovel = null ; 
        else resp[sizeno-start+1].el = null ;
        for(flag=0,i=1;i<len+1&&!flag;i++) if(resp[i]) 
          if(resp[i].el||resp[i].ovel) flag = 1 ; 
        if(!flag) 
        { node = resp[0].el ; node.parentNode.removeChild(node) ; }
      }
      else
      { txt = textshape([this.naturalWidth,this.naturalHeight],
                        photolist.alias) ;
        txt = document.createTextNode(' actually ' + txt) ;
        node.appendChild(txt) ;
      }
    } 
  }

  setdomtitle(photolist.pagetitle) ; 
  body.setAttribute('style',
                    'background:white;margin:6px 0;font-family:'+sty.font) ;
  while(body.firstChild) body.removeChild(body.firstChild) ;

  body.appendChild(domcreate("p",photolist.pagetitle,'style',
        "text-align:center;font-size:140%;margin:2px 0 6px;color:dimgray;")) ; 

  for(sno=0;sno<sect.length;sno++) 
  { item = sect[sno] ;
    body.appendChild(domcreate('p',item.title?item.title:'[Untitled section]',
                  "style","font-size:110%;padding:6px 6px 4px;" +
                          "background:#4a4a4a;color:lightgray;margin:4px 0")) ; 

    // loop over images
    for(ll=item.list,i=0;i<ll.length;i++)
    { item = ll[i] ; 
      sizes = item.sizes ; 
      p = domcreate("p",item.name+(item.serialno>0?' ('+item.serialno+')':''),
                    "style","padding:0 6px 0 6px;margin:4px;color:dimgray") ;
 
      len = sizes.length + 1 ; 
      if(item.hithumb) len += item.hithumb.length ; 

      // dummy item (introductory text), 1 item per size, then thumb/hithumbs
      if(item.extn=='.mp4') { len = 3 ; start = sizes.length-1 ; }
      else start = 0 ; 
      item.resp = new Array(len+1) ; 
      item.resp[0] = { el:p , ovel:null , shape:null } ; 

      for(png=0;png<2&&(png==0||item.overlay);png++)
        for(sizeno=start;sizeno<len;sizeno++)
      { if(sizeno>=sizes.length&&png) continue ;
        if(sizeno==sizes.length) 
        { shape = item.thumbshape ;
          sfx = shape[2] ; 
          jpglink = jpg(item,-1,png) ; 
        }
        else if(sizeno>sizes.length) 
        { j = sizeno - (sizes.length+1) ; 
          sfx = item.hithumb[j].suffix ;
          shape = [ item.thumbshape[0]*item.hithumb[j].scale , 
                    item.thumbshape[1]*item.hithumb[j].scale ] ;
          jpglink = item.filename + sfx + (item.extn=='.mp4'?'.jpg':item.extn) ;
        }
        else if(item.extn=='.mp4') 
        { shape = item.shape ; sfx = '' ; jpglink = item.filename + '.jpg' ; }
        else
        { if(!(shape=imgshape(item,sizeno))) continue ; // no raw 
          sfx = sizes[sizeno].suffix ;
          jpglink = jpg(item,sizeno,png) ; 
        }
        span = domcreate("span",' ♦ ') ; 

        a = domcreate("a",null,'href',jpglink) ;
        a.setAttribute('target','_blank') ;
        sfx += (png?'.png':'') + ': ' + textshape(shape,photolist.alias) ;
        domadd(a,sfx) ; 
        span.appendChild(a) ; 
        if(png) item.resp[sizeno-start+1].ovel = span ;
        else item.resp[sizeno-start+1] = { el:span , ovel:null , shape:shape } ;
        p.appendChild(span) ; 
        img = new Image() ; 
        img.addEventListener
              ('load',responsefactory(item,sizeno,sizes.length,png,len)) ;
        img.src = jpglink ; 
      }
      body.appendChild(p) ; 
    }
  }
  body.appendChild(domcreate('p','\u00a0','style','font-size:110%;'+
     'padding:4px 6px;background:#4a4a4a;color:lightgray;margin:4px 0')) ;
}
/* -------------------------------------------------------------------------- */

function optionparse(s)
{ var name=null,serialno=0,mode='',opt=s.split('&'),i,cat=null ;
  
  for(i=0;i<opt.length;i++)
    if(opt[i].substring(0,6)=='image=') name = opt[i].substring(6) ;
    else if(opt[i].substring(0,9)=='serialno=') serialno = opt[i].substring(9) ;
    else if(opt[i].substring(0,5)=='mode=') mode = opt[i].substring(5) ;
    else if(opt[i].substring(0,4)=='cat=') cat = opt[i].substring(4) ;
    else if(opt[i].length) alert('unrecognised option: '+opt[i]) ; 

  return { name:name , serialno:serialno , mode:mode , cat:cat } ;
}
/* -------------------------------------------------------------------------- */
/*page*/
/* -------------------------------------------------------------------------- */

function pix(xmlfl)
{ var xmldoc , xhttp = new XMLHttpRequest() , parser , field , style ;

  history.replaceState({ind:null},'',location.href) ; 
  // I've forgotten what this is for. without the test on e.state it doesn't 
  // work in safari. I don't know if the check has any consequences.
  window.onpopstate = function (e)
  { if(e.state)
    { if(e.state.ind) display(e.state.ind) ; else tabulate(e.state.pos) ; }
  } ;
  if(xmlfl==undefined||xmlfl.substring(xmlfl.length-4).toLowerCase()!='.xml') 
  { alert('No XML photo list provided') ; throw '' ; }

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

function render()
{ var ind,i,k,s,h,head=document.getElementsByTagName('head')[0],itemno,desc ; 
  var imgforce,title=photolist.title,sect,val,sel,sno,ll,sty,style ;
  render.historylength = history.length ; 

  // parse the url options for the current page
  thispage = location.href ;
  thisid = null ; 
  if((itemno=thispage.indexOf('?'))>=0)
  { query = optionparse(thispage.substring(itemno+1)) ; 
    thispage = thispage.substring(0,itemno) ; 
  } 
  else query = optionparse('') ; 

  ind = thispage.indexOf('#') ; 
  if(ind>=0) 
  { thisid = thispage.substring(ind+1) ; 
    thispage = thispage.substring(0,ind) ; 
  }
  if((ind=thispage.lastIndexOf('/'))>=0) thispage = thispage.substring(ind+1) ; 

  // title taking account of category
  val = getcatval(photolist.title,query.cat) ; 
  if(!val)
  { h = document.getElementsByTagName("title") ;
    if(h.length==0) alert('no title') ; else val = h[0].textContent ; 
  }
  photolist.pagetitle = val ; // should rename pagetitle to title

  // icon 
  val = getcatval(photolist.icon,query.cat) ; 
  if(val) 
  { h = document.createElement('link') ;
    h.setAttribute('rel','shortcut icon') ; 
    h.setAttribute('href',photolist.icon=val) ; 
    head.appendChild(h) ;
  }

  // origin
  val = getcatval(photolist.origin,query.cat) ; 
  if(val) photolist.origin = val ;

  // style
  style = clone(defstyle) ; 
  sty = getcatval(photolist.style,query.cat) ; 
  if(sty) for(field in style) if(sty[field]) style[field] = sty[field] ;
  for(field in cols) style[field] = cols[field] ;
  if(!style.title) style.title = style.fg ;
  if(!style.titlebg) style.titlebg = style.bg ;
  if(!style.titlefont) style.titlefont = style.font ;
  photolist.style = style ; 

  // set body style
  body = document.getElementsByTagName("body")[0] ;
  if(!style.farg) sty = 'background:'+ style.bg ; 
  else if(typeof(style.farg)=='string') sty = 'background:'+ style.farg ; 
  else 
  { s = style.farg ; 
    sty = 'background-image:' + s[0].url + '; background-image: image-set('
    for(i=0;i<s.length;i++) 
      sty += s[i].url + s[i].scale + (i==s.length-1?')':',') ;
  }
  body.setAttribute('style','margin:0;padding:0;font-family:'+style.font+';'+
                            sty+';color:'+style.fg) ; 
  
  // prepare the list
  selcat(photolist,query) ; 
  dolayouts(photolist.sect) ;
  sect = photolist.sect ; 

  // validation?
  if(query.mode.indexOf('v')>=0) return validate() ; 
  if(query.mode.indexOf('g')>=0) return gallerise() ; 

  here = { name:null , serialno:null , ind:null , 
           ncol:{ncol:0,laysize:0,wwid:0} } ;

  window.onresize = pixresize ;
  document.onkeydown = navigate ; 
  document.addEventListener('touchstart',startswipe,false) ;        
  document.addEventListener('touchmove',midswipe,false) ;        
  document.addEventListener('touchend',endswipe,false) ;

  // make up description for meta field
  for(desc=sno=0;sno<sect.length;sno++) for(ll=sect[sno],i=0;i<ll.length;i++)
  { if(desc==0) desc = 'Photos: ' ; else desc += ' | ' ; desc += ll[i].title ; }

  if(query.name&&(itemno=findimage(sect,query.name,query.serialno)))
    display(itemno) ; 
  else tabulate(-1) ; 
}
/* ---------------------------------- swipes -------------------------------- */

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

/* -------------------------------------------------------------------------- */
/*page*/
/* -------------------------------------------------------------------------- */

function tabinfodiv(p,q)
{ var d=document.createElement('div'),i,ll,npix,n2,s,noraw='',norig=null ; 
  var nstarred , nvid , sect=p.sect , rawsect=p.rawsect , cats=p.cats ;
  d.appendChild(toolbox(p,q)) ;

  for(norig=i=0;i<rawsect.length;i++) 
    for(ll=rawsect[i].list,j=0;j<ll.length;j++)
  { norig += 1 ; 
    if( ll[j].rawshape && ll[j].rawshape.length==2
     && ( ll[j].rawshape[0]==0||ll[j].rawshape[1]==0) ) 
      noraw += ' ' + ll[j].name ;
  }

  for(nvid=nstarred=n2=npix=i=0;i<sect.length;i++) 
  { ll = sect[i].list ;
    npix += ll.length ; 
    for(j=0;j<ll.length;j++) 
    { if(ll[j].visibility=='*') nstarred += 1 ; 
      if(ll[j].extn=='.mp4') nvid += 1 ;
    }
    if(sect[i].href) 
    { s = sect[i].title ;
      if(s.length>=9&&s.substring(s.length-8)==' photos)')
      { s = s.substring(0,s.length-8) ; 
        j = s.lastIndexOf('(') ;
        if(j>=0&&isvalidnum(s=s.substring(j+1))) n2 += parseInt(s) ;
      }
    }
  }

  if(sect.length>1) 
  { if(n2) s = ' galleries' ; else s = ' sections' ;
    d.appendChild(document.createTextNode(sect.length+s)) ;
    d.appendChild(document.createElement('br')) ;
  }

  s = npix + ' photos' ;
  if(n2) s += ' (\u2192' + n2 + ')' ;
  else if(norig!=null&&norig>npix) s += ' displayed (out of '+norig+')'  ;
  d.appendChild(document.createTextNode(s)) ;
  d.appendChild(document.createElement('br')) ;

  if(nstarred) 
  { d.appendChild(document.createTextNode(nstarred+' starred')) ;
    d.appendChild(document.createElement('br')) ;
  }
  if(nvid) 
  { d.appendChild(document.createTextNode(nvid+' videos')) ;
    d.appendChild(document.createElement('br')) ;
  }

  if(noraw!='')
  { d.appendChild(document.createTextNode('No raw shape for'+noraw)) ;
    d.appendChild(document.createElement('br')) ;
  }
  d.appendChild(pixhelpdiv(p.style)) ;
  d.appendChild(pixdocspan()) ;
  return d ; 
}
/* -------------------------------------------------------------------------- */

function toolbox(p,q) 
{ var d = document.createElement('div') , i , vis , v , s , res , flag = 0 ; 
  var cats = p.cats , resol = [ 0 , 0.2 , 0.6 , 1 ] ; 
  var rname = [ 'min' , 'default' , 'high' , 'max' ] ; 

  // internal functions
  function catclickfactory(cat,opt) 
  { return function()
    { var ret,thisuri ; 
      genmenu('del') ; 
      here.ind = null ; 
      if(opt==2) { genimage.res = resol[cat] ; retabulate() ; return ; }
      if(opt) q.mode = cat ; 
      else { q.cat = cat ; photolist.pagetitle = getcatval(p.title,cat) ; }
      q.name = null ; 
      // the shortcut icon isn’t present in the dom; changing the origin
      // would be unintuitive
      selcat(p,q) ; 
      dolayouts(p.sect) ;
      thisuri = thispage ;
      if(q.mode||q.cat)
      { thisuri += '?' ;
        if(q.cat) { thisuri += 'cat=' + q.cat ; if(q.mode) thisuri += '&' ; }
        if(q.mode) thisuri += 'mode=' + q.mode ;
      }
      history.pushState({ind:null},'',thisuri) ; 
      retabulate() ; // calls tabulate calls gentable
    }
  }
  function catbox(opt,str,cat)
  { var s = document.createElement('span') ; 
    s.setAttribute('style','color:'+p.style.mlink+';cursor:pointer') ; 
    s.onclick = catclickfactory(cat,opt) ; 
    s.appendChild(document.createTextNode(str)) ;
    return s ;
  }

  // full screen
  s = offerfullscreen( { exit: 'Exit full screen [esc key]' , 
                         enter: 'Enter full screen [f key]' } , p.style.mlink) ;
  if(s) { d.appendChild(s) ; flag = 1 ; } 

  // categories
  if(cats.length) 
  { d.appendChild(document.createTextNode('View category: ')) ;
    for(i=0;i<cats.length;i++) 
    { if(cats[i]==q.cat) domadd(d,q.cat) ; 
      else domadd(d,catbox(0,cats[i],cats[i])) ; 
      domadd(d,' | ') ;
    }
    if(q.cat) domadd(d,catbox(0,'all',null)) ; else domadd(d,'all') ;
    domadd(d,domcreate('br')) ;
    flag = 1 ; 
  }

  // display options
  if(p.vis.star+p.vis.none+p.vis.def>1) 
  { domadd(d,'Display: ') ;
    if(p.vis.star)
    { if(q.mode.indexOf('*')>=0) domadd(d,'starred') ;
      else domadd(d,catbox(1,'starred','*')) ;
      domadd(d,' | ') ;
    }
    if(p.vis.def)
    { if(p.vis.none==0) v = 'all'; else v = 'default' ;
      if(q.mode.indexOf('*')<0&&q.mode.indexOf('a')<0&&q.mode.indexOf('h')<0) 
        domadd(d,v) ; 
      else domadd(d,catbox(1,v,'')) ;
      if(p.vis.none) domadd(d,' | ') ;
    }
    if(p.vis.none)
    { if(q.mode.indexOf('a')>=0) domadd(d,'all') ;
      else domadd(d,catbox(1,'all','a')) ;
      domadd(d,' | ') ;
      if(q.mode.indexOf('h')>=0) domadd(d,'hidden') ;
      else domadd(d,catbox(1,'hidden','h')) ;
    }
    domadd(d,domcreate('br')) ;
    flag = 1 ; 
  }

  // resolution
  if((window.devicePixelRatio||1)>1)
  { d.appendChild(document.createTextNode('Resolution: ')) ;
    if(!genimage.res&&genimage.res!=0) v = 0.2 ; else v = genimage.res ;
    for(i=0;i<4;i++) 
      if(i==0||Math.abs(v-resol[i])<Math.abs(v-resol[res])) res = i ; 
    for(i=0;i<4;i++) 
    { if(i==res) domadd(d,rname[i]) ; else domadd(d,catbox(2,rname[i],i)) ;
      if(i<3) domadd(d,' | ') ;
    }
    domadd(d,domcreate('br')) ;
    flag = 1 ; 
  }
  if(flag) d.setAttribute('style','white-space:nowrap;border-bottom:solid 1px ' 
                       +p.style.mfg+';padding-bottom:3px;margin-bottom:3px') ;

  return d ; 
}
/* -------------------------------------------------------------------------- */

function gallerise()
{ var sect=photolist.sect , sizes , list=[] , item , title ; 
  var sno , i , k , ll , imagedir , s , u , v ;
  var hithumb=null , st="margin:0;font-family:monospace,monospace" ; 

  for(n=sno=0;sno<sect.length;sno++) 
    for(ll=sect[sno].list,n+=ll.length,i=0;i<ll.length;i++)
      if(ll[i].visibility=='*') list.push(ll[i]) ; 

  if(list.length==0) { alert('No starred images') ; return ; }

  imagedir = list[0].filename ; 
  i = imagedir.lastIndexOf('/') ; 
  if(i>=0) imagedir = imagedir.substring(0,i) ; 

  item = list[0] ; 
  s = '<section title="' + photolist.pagetitle + ' (' + n + ' photos)"' ;
  p = document.createElement('p') ; 
  p.setAttribute('style',st) ; 
  p.appendChild(document.createTextNode(s)) ; 
  p.appendChild(document.createElement('br')) ; 

  s = '\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 thumbshape="' + 
          item.thumbshape[2] + '[' + item.thumbshape[0] + ',' + 
                                     item.thumbshape[1] + ']"' ;
  if(u=hi(item)) s += ' hithumb="' + u + '"' ;
  p.appendChild(document.createTextNode(s)) ; 
  p.appendChild(document.createElement('br')) ; 

  function hi(item)
  { var j,k,qf,s='',i,h,sizes=item.sizes ;
    if(item.hithumb) for(h=item.hithumb,j=0;j<h.length;j++)
      s += (s?' ':'') + h[j].suffix + '[' + pixscale(h[j].scale) + ']' ;
    else if(item.shape[0]*item.thumbshape[1]==item.shape[1]*item.thumbshape[0])
      for(j=0;j<sizes.length;j++) if(!sizes[j].type)
    { qf = item.shape[0]*sizes[j].scale / (item.thumbshape[0]*sizes[0].scale) ; 
      s += (s?' ':'') + sizes[j].suffix + '[' + pixscale(qf) + ']' ;
    }
    return s ; 
  }
  s = '\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 imagedir="' + 
      reluri(location.href,item.imagedir) + '"' ;
  p.appendChild(document.createTextNode(s)) ; 
  p.appendChild(document.createElement('br')) ; 

  v = reluri(location.href,photolist.gallery) ;
  if(query.cat) v += '?cat=' + query.cat ;
  s = '\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 href="' + v + '">' ;
  p.appendChild(document.createTextNode(s)) ; 
  p.appendChild(document.createElement('br')) ; 

  for(i=0;i<list.length;i++)
  { item = list[i] ;
    s = '\u00a0\u00a0\u00a0\u00a0<img name="' + item.name + '"' ;
    // check if thumbshape agrees with the section default
    for(j=0;j<3&&item.thumbshape[j]==list[0].thumbshape[j];j++) ;
    if(j<3) 
    { s += ' thumbshape="' + item.thumbshape[2] + 
          '[' + item.thumbshape[0] + ',' + item.thumbshape[1] + ']"' ;
    }

    // provide a hithumb if non-default
    v = hi(item) ; 
    if(v&&v!=u) s += ' hithumb="' + v + '"' ;
    else if(u&&!v) s += ' hithumb=""' ;

    if(item.imagedir!=list[0].imagedir)
    { p.appendChild(document.createTextNode(s)) ; 
      p.appendChild(document.createElement('br')) ; 
      s = '\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0imagedir="' + 
          reluri(location.href,item.imagedir) + '"' ;
    }
    s += '/>' ;
    p.appendChild(document.createTextNode(s)) ; 
    p.appendChild(document.createElement('br')) ; 
  }
  p.appendChild(document.createTextNode('</section>')) ; 
  p.appendChild(document.createElement('br')) ; 
  body.appendChild(p) ; 
}
/* -------------------------------------------------------------------------- */

function gentable(pfx,sfx,caption,sect,colp,clicker,style)
{ var table,tr,td,s,ind,colno,nload,p,item,nbar,i,j,a,nim,str,layind,ll,d ; 
  var padflag,k,img,ngps,legend,sind,ncol,nrow,colour,lflag=0,lmode,gap,sno ; 
  var imgw,imgh,longdim,colwid,gmi,alist,trlist,gmilist,f,g,shortdim,docapt ;
  var laysize = colp.laysize , imgdiv , sizes , prefi ;
  var clickind,itable,alllayout ; 

  function hoverfactory(ind) { return function() { clicker(ind) ; } } 
  function unhoverfactory() 
  { return function() { this.border.color = style.bg ; } } 

  function addrow(div,captionflag,tab) 
  { if(!div) return ;
    var tr = document.createElement('tr') ;
    var td = document.createElement('td') ;
    td.setAttribute('colspan',99999) ;
    if(captionflag)
    { td.setAttribute("style","width:1px;font-size:90%;color:"+style.mg+
                              ";padding:6px 4px 8px") ; 
      td.setAttribute('align','left') ;  
      td.innerHTML = div ; 
    }
    else td.appendChild(div) ; 
    tr.appendChild(td) ; 
    if(tab) tab.appendChild(tr) ; else table.appendChild(tr) ; 
  }
  function hr()
  { var d = document.createElement('div') ; 
    d.setAttribute('style','width:100%;height:1px;background:'+style.mg+
                   ";margin-bottom:4px") ; 
    addrow(d) ; 
  }
  
  table = document.createElement('table') ;
  table.setAttribute('cellspacing','0') ; 
  table.setAttribute('cellpadding','0') ; 
  table.setAttribute('align','center') ; 

  if(pfx) addrow(pfx) ; 
  if(caption) addrow(caption,1) ;

  for(alllayout=1,gentable.nimg=sno=0;sno<sect.length;sno++)
  { if(!sect[sno].layout) alllayout = 0 ; 
    gentable.nimg += sect[sno].list.length ; 
  }

  for(prefi=-1,layout=null,layind=nload=sno=0;sno<sect.length;sno++)
  { item = sect[sno] ; 
    layout = item.layout ;
    ll = item.list ; 
    if(ll.length>0&&prefi<0) prefi = sno ; // prefi is first non-empty section
    if(layout) { sind = laysize[layind++] ; if(sind==null) layout = null ; }
    if(layout)
    { ncol = layout.ncol ; 
      nrow = ll.length / ncol ;
      lflag = layout.flag ; 
      itable = document.createElement('table') ;
      itable.setAttribute('cellspacing','0') ; 
      itable.setAttribute('cellpadding','0') ; 
      itable.setAttribute('align','center') ; 
      itable.setAttribute('style','margin:0px auto') ; 
      if(!lflag) colwid = new Array(ncol) ; 
    }
    else 
    { sind = -1 ; nrow = 0 ; ncol = colp.ncol ; lflag = 0 ; itable = table ; }

    hr() ; 
    tr = document.createElement('tr') ;
    td = document.createElement('td') ;
    td.setAttribute('align','left') ;  
    if(lflag) td.setAttribute('colspan',2*ncol-1) ; 
    else td.setAttribute('colspan',ncol) ;  

    if( item.title || (item.gps&&item.gps.length) || item.caption )
    { p = document.createElement("p") ; 
      p.setAttribute("style","font-size:110%;padding-top:8px;"+
                             "padding-bottom:4px;padding-left:4px") ; 
      if(item.title&&(!item.gps||!item.gps.length))
      { s = document.createTextNode(item.title) ;
        if(item.href)
        { a = document.createElement('a') ; 
          a.setAttribute('href',item.href) ; 
          a.setAttribute('style','color:'+style.fg) ; 
          a.appendChild(s) ; 
          alist = [a] ;
          s = a ; 
        }
        p.appendChild(s) ; 
      }
      else if(item.gps&&item.gps.length) // we have GPS tracks
      { if(item.title) 
          p.appendChild(document.createTextNode(item.title+' : ')) ;
        if(item.gps.length==1) // one track only
        { a = document.createElement('a') ; 
          a.setAttribute('href',item.gps[0]) ; 
          a.setAttribute('style','font-size:90%') ; 
          a.setAttribute('target','_blank') ; 
          a.appendChild(document.createTextNode('[GPS track ')) ;
          a.appendChild(newtabdiv(style.fg,style.bg)) ; 
          a.appendChild(document.createTextNode('\u202f]')) ;
          p.appendChild(a) ;
        }
        else // several tracks: need to differentiate them
        { s = document.createElement('span') ; 
          s.setAttribute('style','font-size:90%') ; 
          for(ngps=i=0;i<item.gps.length;i++)
            if(item.gps[i]!='-'&&item.gps[i]!='–') ngps += 1 ; 
          if(ngps==1) legend = '[GPS track: ' ; 
          else legend = '[GPS tracks: ' ;
          s.appendChild(document.createTextNode(legend)) ;
          for(nbar=i=0;i<ll.length;i++) if(ll[i].barred=='|') nbar += 1 ; 
          if(nbar+1>=item.gps.length&&!item.nomatch) // square icon links
            for(gpsno=0,i=0;i<ll.length;gpsno++)
          { if(item.gps[gpsno]=='-'||item.gps[gpsno]=='–')
              a = document.createElement('span') ; 
            else
            { a = document.createElement('a') ; 
              a.setAttribute('href',item.gps[gpsno]) ; 
              a.setAttribute('target','_blank') ; 
            }
            for(nim=1,i++;i<ll.length&&ll[i].barred!='|';i++) nim += 1 ;
            for(str = '',j=0;j<nim;j++)
            { str += '■' ; if(j<nim-1) str += '-' ; }
            a.appendChild(document.createTextNode(str)) ;
            s.appendChild(a) ;
            if(i<ll.length) s.appendChild(document.createTextNode(' | ')) ;
          }
          else for(i=0;i<item.gps.length;i++) // numbered links [(1), (2)...
            if(item.gps[i]!='-'&&item.gps[i]!='–')
          { a = document.createElement('a') ; 
            a.setAttribute('href',item.gps[i]) ; 
            a.appendChild(document.createTextNode(' ('+(i+1)+')')) ;
            s.appendChild(a) ;
          }
          else s.appendChild(document.createTextNode(' –')) ;
          s.appendChild(document.createTextNode(' : ')) ;
          s.appendChild(newtabdiv(style.fg,style.bg)) ; 
          s.appendChild(document.createTextNode('\u202f]')) ;
          p.appendChild(s) ;
        }
      } // end 'we have GPS tracks'
      if(item.caption) 
      { s = document.createElement('div') ; 
        s.innerHTML = item.caption ;
        s.setAttribute("style","color:"+style.mg+";font-size:85%;"+
                       "padding-top:4px;text-indent:16px;padding-left:4px") ; 
        p.appendChild(s) ; 
      }
      td.appendChild(p) ; 
    } // end check for defined title
    else td.setAttribute('height','6px') ; 
    tr.appendChild(td) ; 
    if(item.id) tr.setAttribute('id',item.id) ;
    // element to scroll to when returning from an image
    if(sno) item.scrollelement = tr ; else item.scrollelement = null ; 

    if(item.href) 
    { trlist = [tr] ; 
      gmilist = [] ; 
      if(ll.length>ncol)
      { ll = new Array(ncol) ; 
        for(nim=i=0;i<item.list.length;i++)
          if(Math.random()<(ncol-nim)/(item.list.length-i)) 
            ll[nim++] = item.list[i] ;
      }
    }
    itable.appendChild(tr) ; 

    /* ----------------------------- loop over images ----------------------- */

    for(gap=null,ind=0;ind<ll.length;ind++) 
    { item = ll[ind] ; 
      if(layout) sizes = layout.sizes ; else sizes = item.sizes ; 

      // extra padding at the bottom before a title row
      if(0==ind%ncol) 
      { tr = document.createElement('tr') ; 
        padflag = 4 ; 
        if(sno<sect.length-1&&ind+ncol>=ll.length)
        { padflag = 8 ; if(ncol>5) padflag += 2*(ncol-5) ; }
      } 
      // lmode is a per-layout parameter specifying the shape of a tessellation
      if(lflag==2&&ind==0) 
      { if(item.shape[0]>item.shape[1]) lmode = 'l' ; else lmode = 'p' ; } 

      clickind = ind ; 
      if(lflag==2&&lmode=='p'&&ind%(2*ncol)>=ncol)
      { if(ind%ncol==0) clickind = ind + (ncol-1) ; 
        else clickind = ind - 1 ; 
        item = ll[clickind] ;
      }

      // generate the image
      function loadfactory(prefi)
      { return function()
        { gentable.nimg -= 1 ; 
          if(gentable.nimg==0) 
            doim(genimage(sect[prefi].list[0],null,{sizes:sizes})) ; 
        } ;
      } ;
      img = genimage(item,sind,{loadfunc:loadfactory(prefi),sizes:sizes}) ; 
      imgw = img.shape[0] ; 
      imgh = img.shape[1] ;
      img = doim(img).img ; 
      if(layout&&(!lflag)&&!colwid[ind%ncol]) colwid[ind%ncol] = imgw ;

      // initialise cell dimensions now we’ve seen the first image
      // gap is a per-section size difference needed for tessellated layouts
      if(lflag&&gap==null) 
      { if(lflag==1)
        { if(sind<0) longdim = ll[ind+1].thumbshape[0] ;
          else longdim = ( ll[ind+1].shape[0] * sizes[sind].scale ) 
                                                           / sizes[0].scale ;
          if(longdim>imgw) shortdim = imgw ; 
          else { shortdim = longdim ; longdim = imgw ; }
          gap = longdim - shortdim ; 
        }
        else if(lflag==2)
        { if(imgw>imgh) { gap = imgw - imgh ; longdim = imgw ; }
          else { gap = imgh - imgw ; longdim = imgh ; }
        }
        for(i=0;i<2*ncol-1;i++)
        { d = document.createElement('col') ; 
          if(i&1) k = gap ; else k = 19 + longdim - gap ; 
          d.setAttribute('style','width:'+k+'px') ; 
          itable.appendChild(d) ;
        }
      }

      // empty cell for tessellated portrait-style layout
      if(lflag==2&&lmode=='p'&&ind%(2*ncol)==ncol) 
      { td = document.createElement('td') ;
        k = gap + (ncol-2) * (19+imgh) ; 
        td.setAttribute('style','width:'+k+'px;height:'+gap+'px') ;
        td.setAttribute('colspan',2*ncol-3) ; 
        tr.appendChild(td) ;
      }

      // generate the td and set its attributes
      td = document.createElement('td') ;
      td.setAttribute('align','center') ;  
      s = 'width:'+(19+imgw)+'px;height:'+(19+imgh)+'px;' ;
      if((!layout)&&(item.barred=='|'&&ind%ncol>0))
        s = 'border-left:1px solid '+style.mg+';' ;
      if(lflag==2)
      { if(item.shape[0]<item.shape[1]) td.setAttribute('rowspan',2) ;
        else td.setAttribute('colspan',2) ; 
      }
      else if(lflag==1)
        if(imgw==longdim) td.setAttribute('colspan',2) ;
      td.setAttribute('style',s) ;

      // set the image attributes
      if(alllayout||sect[sno].href) colour = style.bg ;  
      else if(item.visited) colour = style.visited ; 
      else colour = style.link ;
      s = 'cursor:pointer;border:1px solid ' + colour ;
      s += ';padding:4px;margin:4px 4px ' + padflag + 'px' ;
      // vertical bar: margin is t-r-b-l or t-lr-b
      if((!layout)&&(ind%ncol==0||item.barred!='|')) s += ' 5px' ;
      if(sect[sno].href) // this is for thumbs in galleries
      { img.setAttribute('style',s) ; 
        gmilist.push(img) ;
        imgdiv = document.createElement('a') ; 
        imgdiv.setAttribute('href',sect[sno].href) ; 
        imgdiv.appendChild(img) ; 
      }
      else
      { imgdiv = document.createElement('div') ; 
        imgdiv.addEventListener('click',hoverfactory([sno,clickind]) ) ; 
        img.setAttribute('style','margin:auto') ;         
        imgdiv.setAttribute('style',s+
                            ';position:relative;width:'+imgw+';height:'+imgh) ; 
        imgdiv.appendChild(img) ; 
        if(alllayout)
        { img.addEventListener('mouseout',function() 
                                { this.style.borderColor = style.bg ; } ) ; 
          if(item.visited) img.addEventListener('mouseover',function() 
                                { this.style.borderColor = style.visited ; } ) ; 
          else img.addEventListener('mouseover',function() 
                                { this.style.borderColor = style.link ; } ) ; 
        }
        if(item.texttitle) imgdiv.setAttribute("title",item.texttitle) ; 
        if(item.extn==".mp4") imgdiv.appendChild(mp4icon()) ;
        img = imgdiv ; 
      }
      td.appendChild(imgdiv) ; 
      // add an icon to video thumbs

      tr.appendChild(td) ; 
      if(ind==ll.length-1||(ind+1)%ncol==0) 
      { if(sect[sno].href) trlist.push(tr) ; itable.appendChild(tr) ; }

      // empty cell for tessellated landscape-style layout
      if(lflag==2&&lmode=='l'&&ind%(2*ncol)==ncol) 
      { td = document.createElement('td') ;
        k = gap + (ncol-2) * (19+imgw) ; 
        td.setAttribute('style','width:'+k+'px;height:'+gap+'px') ;
        td.setAttribute('colspan',2*ncol-3) ; 
        tr.appendChild(td) ;
      }

      if(lflag==2&&ind%(2*ncol)==ncol)
      { itable.appendChild(tr) ; tr = document.createElement('tr') ; }

      // caption for layout
      docapt = alllayout && ind==ll.length-1 && !layout.notitle ;
      if(docapt) for(docapt=i=0;i<ll.length&&!docapt;i++)
        if(ll[i].title) docapt = 1 ; 
      if(docapt) for(i=0;i<nrow;i++)
      { for(tr=document.createElement('tr'),j=0;j<ncol;j++)
        { td = document.createElement('td') ;
          d = document.createElement('div') ;
          if(!lflag) k = colwid[j] ; 
          else if(lflag==1)
          { if((imgw==longdim&&j)||(imgw!=longdim&&!j))
            { td.setAttribute('colspan',2) ; k = longdim ; }
            else k = longdim - gap ;
          }
          else if(lflag==2)
          { if((lmode=='p'&&j!=ncol-1)||(lmode=='l'&&j!=0))
            { td.setAttribute('colspan',2) ; k = longdim ; }
            else k = longdim - gap ;
          }
          d.setAttribute('style','color:'+style.mg+';white-space:nowrap;'+
                                 'width:'+k+'px;overflow-x:hidden;'+
                                 'font-size:90%;text-align:center;') ;
          td.setAttribute('style','padding:3px 9px;') ;
          s = ll[j+ncol*i].title ;
          d.appendChild(document.createTextNode(s?s:'–')) ;
          td.appendChild(d) ;
          tr.appendChild(td) ;
        }
        itable.appendChild(tr) ; 
      }
      if(docapt)
      { d = document.createElement('div') ;
        d.setAttribute('style','width:100%;height:4px') ; 
        addrow(d,null,itable) ; 
      } // end loop for(i=0;i<nrow;i++)
    } // end loop over images

    if(sect[sno].href)
    { function hovercraft(gmilist,alist)
      { return function()
        { var i ; 
          for(i=0;i<gmilist.length;i++) 
            gmilist[i].style.borderColor = 'initial' ;
          for(i=0;i<alist.length;i++) alist[i].style = 'initial' ;
        } ;
      }
      function unhovercraft(gmilist,alist)
      { return function()
        { var i ; 
          for(i=0;i<gmilist.length;i++) 
            gmilist[i].style.borderColor = style.bg ;
          for(i=0;i<alist.length;i++) alist[i].style.color = style.fg ;
        } ;
      }
      f = hovercraft(gmilist,alist) ; 
      g = unhovercraft(gmilist,alist) ;
      for(i=0;i<trlist.length;i++) 
      { trlist[i].onmouseover = f ; trlist[i].onmouseout = g ; }
    }

    if(layout) addrow(itable) ; 
  }
  hr() ; 
  if(sfx) addrow(sfx) ; 

  return table ;
}
/* -------------------------------------------------------------------------- */

function dolayouts(r)
{ var sno,ll,layout,s,notitle,m,lcol,lrow,lht,i,j,c,lht,item,sumw,sumwt ; 
  var maxh,maxht,k,w,sh0,sh1,flag,w0,w1,sizes,len ;

  function matches(sh0,sh1,parity)
  { if( sh0[0]==sh1[parity] && sh0[1]==sh1[1-parity] ) return 1 ; 
    else return 0  ;
  }

  for(sno=0;sno<r.length;sno++) if(layout=r[sno].layoutstr)
  { ll = r[sno].list ; 
    // parse options
    s  = layout.split(':') ;
    for(notitle=m=lcol=lrow=lht=i=0;i<s.length;i++)
      if((c=s[i].charAt(s[i].length-1))=='%')
    { lht = s[i].substring(0,s[i].length-1) ; 
      if(!isvalidnum(lht)) alert("Illegal height in layout "+layout) ; 
      lht = parseFloat(lht)/100 ; 
    }
      else if(c=='n'||c=='t') for(k=0;k<s[i].length;k++)
    { if((c=s[i].charAt(k))=='n') notitle = 1 ; 
      else if(c=='t') m = 1 ; 
      else alert('Unrecognised layout mode '+s[i]) ; 
    }
      else
    { if(isvalidnum(s[i]))
      { lcol = parseInt(s[i]) ; lrow = Math.floor(ll.length/lcol) ; }
      else 
      { c = s[i].split('x') ; 
        if(c.length!=2) alert("Uninterpreted field in layout "+layout) ; 
        if(!isvalidnum(c[0])||!isvalidnum(c[1])) 
          alert("Illegal layout "+layout) ; 
        lrow = parseInt(c[0]) ; 
        lcol = parseInt(c[1]) ; 
      }
      if(ll.length%(lrow*lcol)) 
        alert(layout+' is illegal for '+ll.length+' images') ;
    }
    lrow = ll.length / lcol ; 

    // see if we can use thumbs as normal images
    for(c=1,i=1;c&&(!m)&&i<ll.length;i++)
    { item = ll[i] ; 
      if(!item.shape) { m = 1 ; continue ; }
      if( item.thumbshape[0]*ll[0].shape[0]!=ll[0].thumbshape[0]*item.shape[0]  
       || item.thumbshape[1]*ll[1].shape[1]!=ll[1].thumbshape[1]*item.shape[1] )
        c = 0 ; 
    }

    // produce a consistent set of sizes
    for(len=i=0;i<ll.length;i++) 
      if(ll[i].sizes.length>len) len = ll[i].sizes.length ; 
    sizes = new Array(len+1) ; 
    sizes[0] = { n:1 } ; 
    for(item=ll[0].sizes,len=1,i=0;i<item.length;i++) if(item[i].type!='raw')
    { sizes[len++] = item[i] ; sizes[len-1].n = 1 ; }
    for(i=1;i<ll.length;i++) 
    { item = ll[i].sizes ; 
      for(j=0;j<item.length;j++) if(item[j].type!='raw') 
      { for(k=1;k<len;k++) 
          if(sizes[k].scale==item[j].scale&&sizes[k].suffix==item[j].suffix)
            break ;
        if(k<len) sizes[k].n += 1 ; 
      }
      if( ( ll[i].thumbshape[0]==ll[0].thumbshape[0] 
         && ll[i].thumbshape[1]==ll[0].thumbshape[1] )
       || ( ll[i].thumbshape[0]==ll[0].thumbshape[1] 
         && ll[i].thumbshape[1]==ll[0].thumbshape[0] ) ) 
        sizes[0].n += 1 ; 
    }

    if(sizes[0].n!=ll.length) 
    { if(m) alert("illegal thumbnail sizes for layout "+r[sno].title) ; 
      c = 0 ; 
    }

    if(sizes[1].n!=ll.length) alert("illegal sizes for layout "+r[sno].title) ;
    for(k=0,i=1;i<len;i++) if(sizes[i].n==ll.length) sizes[k++] = sizes[i] ; 
    sizes.length = k ; 
    console.log(sizes) ; 

    // compute sums and maxes used for layout dimensions
    sumw = sumwt = maxh = maxht = null ; 
    maxw = new Array(lcol) ; 
    if(lcol>ll.length) alert("A layout with "+lcol+" columns is being applied"+
       " to a set of just "+ll.length+" images") ; 
    if(m||c) 
    { for(maxht=i=0;i<lcol;i++) 
      { maxw[i] = 0 ; w = ll[i].thumbshape[1] ; if(w>maxht) maxht = w ; }
      for(k=0;k<lrow;k++) for(i=0;i<lcol;i++) 
      { w = ll[i+lcol*k].thumbshape[0] ; if(w>maxw[i]) maxw[i] = w ; } 
      for(sumwt=i=0;i<lcol;i++) sumwt += maxw[i] ;
    }
    if(!m) 
    { for(maxh=i=0;i<lcol;i++) 
      { maxw[i] = 0 ; w = ll[i].shape[1] ; if(w>maxh) maxh = w ; }
      for(k=0;k<lrow;k++) for(i=0;i<lcol;i++) 
      { w = ll[i+lcol*k].shape[0] ; if(w>maxw[i]) maxw[i] = w ; } 
      for(sumw=i=0;i<lcol;i++) sumw += maxw[i] ;
    }

    // check whether we can do the tessellating layout
    flag = 0 ; 
    if(lrow==2&&lcol>=2) 
    { if(m) sh0 = ll[0].thumbshape ; else sh0 = ll[0].shape ; 
      if(sh0[0]!=sh0[1]) flag = 2 ; 
    }
    if(flag) for(i=0;i<ll.length;i+=lrow*lcol) 
    { if(m) { sh0 = ll[i].thumbshape ; sh1 = ll[0].thumbshape ; }
      else { sh0 = ll[i].shape ; sh1 = ll[0].shape ; }
      // compare this instance of layout with first instance in the section
      if((!matches(sh0,sh1,0))&&!matches(sh0,sh1,1)) { flag = 0 ; break ; }

      // now compare all shapes within the instance
      if(m) sh1 = ll[i+lcol-1].thumbshape ; else sh1 = ll[i+lcol-1].shape ;
      if(!matches(sh0,sh1,1)) { flag = 0 ; break ; }
      if(m) sh1 = ll[i+lcol].thumbshape ; else sh1 = ll[i+lcol].shape ;
      if(!matches(sh0,sh1,1)) { flag = 0 ; break ; }
      if(m) sh1 = ll[i+2*lcol-1].thumbshape ; else sh1 = ll[i+2*lcol-1].shape ;
      if(!matches(sh0,sh1,0)) { flag = 0 ; break ; }

      if(sh0[0]<sh0[1]) sh0 = [ sh0[1] , sh0[0] ] ; // landscape version
      for(k=1;k<lcol-1;k++)
      { if(m) sh1 = ll[i+k].thumbshape ; else sh1 = ll[i+k].shape ;
        if(!matches(sh0,sh1,0)) { flag = 0 ; break ; }
        if(m) sh1 = ll[i+k+lcol].thumbshape ; else sh1 = ll[i+k+lcol].shape ;
        if(!matches(sh0,sh1,0)) { flag = 0 ; break ; }
      }
    }
    // and at this point flag is 2 if we should tessellate
    if(flag==0&&lcol==2&&lrow>=2) 
    { if(m) { sh0 = ll[0].thumbshape[0] ; sh1 = ll[1].thumbshape[0] ; }
      else { sh0 = ll[0].shape[0] ; sh1 = ll[1].shape[0] ; }
      if(sh0!=sh1) for(flag=1,i=0;flag&&i<ll.length;i+=2)
      { if(m) { w0 = ll[i].thumbshape[0] ; w1 = ll[i+1].thumbshape[0] ; }
        else { w0 = ll[i].shape[0] ; w1 = ll[i+1].shape[0] ; }
        if((!(w0==sh0&&w1==sh1))&&!(w0==sh1&&w1==sh0)) flag = 0 ;
      }
    }

    // default layout row ht
    if(!lht) 
    { if(flag==2) 
      { lht = ll[0].shape[0]/(ll[0].shape[0]+ll[0].shape[1]) ; 
        if(lht<0.5) lht = 1 - lht ; 
      }
      else lht = 1/lrow ; 
      lht *= 0.95 ;
    }

    r[sno].layout = { ncol:lcol , ht:lht , mustthumb:m , canthumb:c ,
                      flag:flag , notitle:notitle , sizes:sizes ,
                      sumwt:sumwt , sumw:sumw , maxht: maxht , maxh:maxh } ;
  }
}

• domadd     • domcreate     • enterfullscreen     • exitfullscreen     • queryfullscreen     • querycanfullscreen     • isvalidnum     • clone     • pixscale     • reluri     • parsetitle     • setdomtitle     • advance     • getphotolist     • geturialias     • dourialias     • genobject     • genimgobject     • getxmlsize     • getxmlsizes     • imgshape     • sparepix     • gethincr     • jpg     • getsize     • getmp4shape     • genimage     • div     • doim     • findimage     • thumb     • showimg     • dothumbs     • loadpix     • function     • genpic     • navcell     • menufactory     • resize     • setrepeater     • zonkfactory     • rescale     • imgsize     • reres     • setimgpos     • expandcaption     • setimg     • loadfactory     • makecaption     • uncaption     • photoinfodiv     • genmenu     • drawmenuicon     • displaymenu     • delmenu     • offerfullscreen     • getcatval     • pixhelpdiv     • addcell     • pixdocspan     • pixinfodiv     • shapelist     • wxh     • newtabdiv     • genrect     • genpoly     • startswipe     • midswipe     • endswipe     • selcat     • keepquery     • mp4icon

// www.masterlyinactivity.com/software/pix.html
var pixlib = 1 ; 
var xcols = { link: '#66aaaa' , visited: '#cc3388' , active: '#404040' ,
              mlink:'#2244cc' , mvisited:'#cc3388' , mactive:'#808080'} ; 
var xstyle = { fg:'silver' , bg:'black' ,  mg: '#a4a4a4' , 
               mfg:'black' , mbg:'white' , farg:null , 
               font:'helvetica' , titlefont:'helvetica' , 
               title:null   , titlebg:null } ; 
var defstyle = clone(xcols,xstyle) ; 

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

function domadd(div,txt) 
{ if(typeof txt=='string'||typeof txt=='number') 
    div.appendChild(document.createTextNode(txt)) ; 
  else if(txt) div.appendChild(txt) ; 
}
function domcreate(type,txt,p1,p2) 
{ var div = document.createElement(type) ; 
  if(p1&&p2) div.setAttribute(p1,p2) ; 
  if(txt) domadd(div,txt) ; 
  return div ; 
}
function enterfullscreen() 
{ var doo = 1 ; 
  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() ;
  else doo = 0 ; 
}
function exitfullscreen() 
{ if(document.exitFullscreen) document.exitFullscreen() ;
  else if(document.mozExitFullScreen) document.mozExitFullScreen() ;
  else if(document.webkitExitFullscreen) document.webkitExitFullscreen() ;
  else if(document.msExitFullscreen) document.msExitFullscreen() ;
}
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 ;
}
function isvalidnum(x) { return !isNaN(parseFloat(x)) && isFinite(x) ; }
function clone(x,y) 
{ var i,z={} ; 
  if(x) for(i in x) z[i] = x[i] ; 
  if(y) for(i in y) z[i] = y[i] ; 
  return z ; 
}
function pixscale(q)
{ var k , scl = q.toFixed(2) ; 
  for(k=scl.length;k>1&&(scl.charAt(k-1)=='0'||scl.charAt(k-1)=='.');k--) ;
  return scl.substring(0,k) ;
}
/* ---------------------------- 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) { if(u1) return u2.substring(3) ; else return u2 ; }
    u2 = u2.substring(3) ; 
    u1 = u1.substring(0,last) ; 
  }
  return u1 + '/' + u2 ; 
}
/* --------------------------- parse section title  ------------------------- */

function parsetitle(sect)
{ var s = sect.title , ss , j ;
  if(!sect.href||s.length<9||s.substring(s.length-8)!=' photos)') 
    return [ s , null ] ;
  s = s.substring(0,s.length-8) ; 
  j = s.lastIndexOf('(') ;
  if(j<0||!isvalidnum(ss=s.substring(j+1))) return [ s , null ] ;
  else 
  { while(j>0&&s.charAt(j-1)==' ') j -= 1 ; 
    return [ s.substring(0,j) , parseInt(ss) ] ;
  }
}
/* ------------------------------- settitle --------------------------------- */

function setdomtitle(newtitle) 
{ var h , t=document.getElementsByTagName('title') ;
  if(t.length==0)
  { h = document.getElementsByTagName('head') ;
    if(h.length==0) h = domcreate('head') ; else h = h[0] ;
    domadd(h,t=domcreate('title')) ;
  }
  else t = t[0] ; 
  while(t.childNodes.length>0) t.removeChild(t.firstChild) ; 
  if(newtitle) domadd(t,newtitle) ; 
}
/* -------------------------------------------------------------------------- */

function advance(pl,si,dirn) 
{ var sno , ind ;
  if(si) { sno = si[0] ; ind = si[1] ; } 
  else if(dirn<0) { sno = pl.sect.length ; ind = 0 ; }
  else { sno = 0 ; ind = -1 ; }
  if(dirn<0)
  { if(ind>0) return [sno,ind-1] ; 
    else if(sno>0) return [ sno-1 , pl.sect[sno-1].list.length-1 ] ; 
    else return null ; 
  }
  else
  { if(ind<pl.sect[sno].list.length-1) return [sno,ind+1] ;
    else if(sno<pl.sect.length-1) return [sno+1,0] ; 
    else return null ;
  }
}
/* -------------------------------------------------------------------------- */

function getphotolist(xmldoc,baseuri)
{ var tree,i,kids,j,k,item,links=[],hithumb=null,pixp=null,imagedir=null ;
  var aliases=[],ll=[],s,ss,sss=[],title=[],ic=[],cat,img,orp=[],sty=[],sdef ;
  var iname,visibility,cats=[],sect=[],sno,thind,sss,caption=[],c,field,scl ;
  var r0,r1,R,ind,umin,umax,base={},sectitem,q,sind ; 
  var sh={shape:null,rawshape:null,thumbshape:null} ; 
  var imgfields =  ['name','title','caption','display','overlay','id'] ;
  var sectfields = ['title','caption','gps','id','layout','href'] ;
  var heritable  = ['imagedir','extn','cat','sizes',
                    'shape','rawshape','thumbshape','hithumb'] ;
  imgfields  =  imgfields.concat(heritable) ; 
  sectfields = sectfields.concat(heritable) ; 

  // extract fields
  tree = xmldoc.getElementsByTagName('photolist')[0].childNodes ;
  for(i=0;i<tree.length;i++)
  { iname = tree[i].nodeName ; 
    if(iname=='base') base = genobject(tree[i],heritable) ; 
    else if(iname=='link') 
      links.push(genobject(tree[i],['name','href','cat','display','type'])) ;
    else if(iname=='style') 
    { item = genobject(tree[i],['bg','mg','fg','font','mbg','mfg','farg',
                                  'title','titlebg','titlefont','cat']) ;
      sty.push({style:item,cat:item.cat}) ;
    }
    else if(iname=='title') 
    { c = genobject(tree[i],['cat']) ;
      if(c.cat) title.push({text:tree[i].textContent,cat:c.cat}) ;
      else title.push({text:tree[i].textContent}) ; // for Safari
    }
    else if(iname=='caption') 
    { c = genobject(tree[i],['cat']) ;
      if(c.cat) caption.push({text:tree[i].textContent,cat:c.cat}) ;
      else caption.push({text:tree[i].textContent}) ; // for Safari
    }
    else if(iname=='icon') ic.push(genobject(tree[i],['href','cat'])) ;
    else if(iname=='pixpage') 
    { alert("pixpage tag no longer supported (use ‘gallery’)") ; 
      pixp = tree[i].getAttribute('href') ;
    }
    else if(iname=='gallery') pixp = tree[i].getAttribute('href') ;
    else if(iname=='origin') 
      orp.push(genobject(tree[i],['href','cat'])) ;
    else if(iname=='imagedir') 
    { alert("imagedir tag no longer supported (use ‘base’)") ; 
      imagedir = tree[i].getAttribute('href') ;
    }
    else if(iname=='alias') 
      aliases.push(genobject(tree[i],['name','shape','uri'])) ;
    else if(iname=='sizes') 
    { for(ss=[],kids=tree[i].childNodes,j=0;j<kids.length;j++)
        if(kids[j].nodeName=='size')
          ss.push(genobject(kids[j],['suffix','scale','type','fontsize'])) ;
      sss.push({name:tree[i].getAttribute('name') , sizes:ss}) ; 
    }
    else if(iname=='section') 
    { if(ll.length) // preceding images not part of a section
      { sect.push(genimgobject(null,sectfields)) ; 
        sect[sect.length-1].list = ll ; 
      }
      sect.push(genimgobject(tree[i],sectfields)) ;
      for(ll=[],kids=tree[i].childNodes,j=0;j<kids.length;j++)
        if(kids[j].nodeName=='img') ll.push(genimgobject(kids[j],imgfields)) ; 
      sect[sect.length-1].list = ll ; 
      ll = [] ; 
    }
    else if(iname=='img') ll.push(genimgobject(tree[i],imgfields)) ;
  }
  if(ll.length) sect.push( { title:null, gps:null,    cat:null,
                             id:null,    layout:null, list:ll } ) ; 

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

  // the following line seems to allow for an alias of an alias - can I GRO it?
  for(i=0;i<aliases.length;i++) if(aliases[i].shape)
    aliases[i].shape = getxmlsize(aliases[i].shape) ;

  // substitute GPS aliases
  for(sno=0;sno<sect.length;sno++) geturialias(sect[sno],'gps',aliases) ; 

  if(!base.imagedir) base.imagedir = imagedir ; 

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

  // compute some values for each set of sizes
  for(sdef=-1,sno=0;sno<sss.length;sno++) if(ss = sss[sno].sizes) 
  { if(!sss[sno].name) sdef = sno ;
    // fill in the fontsize field in sizes
    for(R=k=ind=0;ind<ss.length;ind++) if(!ss[ind].type) 
    { R += Math.log(ss[ind].scale) ; k += 1 ; }
    if(k) R /= k ;

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

    for(umax=umin=null,ind=0;ind<ss.length;ind++) if(!ss[ind].type)
    { if(umin==null||ss[ind].fontsize<umin) umin = ss[ind].fontsize ;
      if(umax==null||ss[ind].fontsize>umax) umax = ss[ind].fontsize ;
    }
    sss[sno].umin = umin ; 
    for(ind=0;ind<ss.length;ind++) if(ss[ind].type=='raw'&&!ss[ind].fontsize) 
      ss[ind].fontsize = umax ; 

    // find index of thumb, count of hithumb in sizes
    for(sss[sno].hithind=0,sss[sno].thind=-1,i=1;i<ss.length;i++) 
      if(ss[i].type=='thumb') sss[sno].thind = i ; 
      else if(ss[i].type=='hithumb') sss[sno].hithind += 1 ; 
  }
  /* ------------------------------------------------------------------------ */

  // split out starred field, accumulate cats, fill in shapes etc
  for(sno=0;sno<sect.length;sno++)
  { item = sect[sno] ;
    // inherit heritables
    for(j=0;j<heritable.length;j++) 
    { field = heritable[j] ; 
      if(!item[field]&&item[field]!='') item[field] = base[field] ; 
    }
    item.layoutstr = item.layout ; 
    item.layout = null ; 
    sectitem = item ; 

    for(ll=item.list,i=0;i<ll.length;i++) 
    { item = ll[i] ; 

      // inherit heritables; process extn and imagedir->filename
      for(j=0;j<heritable.length;j++) 
      { field = heritable[j] ; 
        if(!item[field]&&item[field]!='') item[field] = sectitem[field] ; 
      }
      if(item.extn) item.extn = '.' + item.extn ; else item.extn = '.jpg' ;
      if(item.imagedir) item.filename = item.imagedir + '/' + item.name ; 
      else item.filename = item.name ; 

      // find the sizes
      if(!item.sizes) item.sizes = sectitem.sizes ; 
      if(item.sizes) 
        for(sind=0;sind<sss.length&&item.sizes!=sss[sind].name;sind++) ;
      if(!item.sizes||sind==sss.length) sind = sdef ; 
      if(sind==null) { alert('no sizes for '+item.name) ; throw '' ; }
      if(sind>=0&&sss[sind].sizes) 
      { item.sizes = ss = sss[sind].sizes ; thind = sss[sind].thind ; }
      else { ss = [] ; thind = null ; }

      // split display field into visibility and display
      v = null ; 
      if(item.display=='*') { item.display = null ; v = '*' ; }
      else if(item.display=='|*') { item.display = '|' ; v = '*' ; }
      else if(item.display=='none') { item.display = null ; v = 'none' ; }
      else if(item.display=='|none') { item.display = '|' ; v = 'none' ; }
      else if(item.display&&item.display!='|') 
        alert('Illegal display value “'+item.display+'” for '+item.name) ; 
      item.visibility = v ;

      // accumulate categories
      if((cat=item.cat)) 
      { for(j=0;j<cats.length&&cats[j]!=cat;j++) ;
        if(j==cats.length) cats.push(cat) ; 
      }

      // parse shape, rawshape, thumbshape and hithumb, substituting from alias
      for(j in sh) if(item[j]) // shape/thumbshape/rawshape
      { item[j] = getxmlsize(item[j],aliases) ;
        if(!item[j][1]) alert('Illegal '+j+' for '+item.name) ;
      }
      if(item.hithumb) item.hithumb = getxmlsizes(item.hithumb) ; 

      // fill in thumbshape as [w,h,sfx,fontsize] for all items
      if(!item.thumbshape)
      { if(thind<0) { alert('no thumbshape for '+item.name) ; throw '' ; }
        if(!item.shape) { alert('no shape for '+item.name) ; throw '' ; }
        r0 = ss[thind].scale / ss[0].scale ; 
        for(item.thumbshape=[0,0,0,0],k=0;k<2;k++) 
          item.thumbshape[k] = Math.floor(0.5+item.shape[k]*r0) ; 
      }
      if(!item.thumbshape[2]) 
      { if(thind>=0) item.thumbshape[2] = ss[thind].suffix ;
        else { alert('no thumbshape suffix for '+item.name) ; throw '' ; }
      }
      if(!item.thumbshape[3]&&sind>=0) item.thumbshape[3] = sss[sind].umin ; 

      // The rule for srcsets will be this:
      //    o. if the item has a hithumb, then offer it; 
      //    o. else, if the thumb has the same aspect ratio as regular images,
      //       offer all the regular images;
      //    o. else don't offer a srcset at all.
      // When we get here, item.hithumb may be null/undef, in which case we
      // choose an appropriate default, or '', in which case we may have a 
      // hithumb for the base or section but have overridden it for the item,
      // in which case we do *not* want values from the hithumb series in sizes,
      // since the user could have got that effect by repeating them for the 
      // item, and may or may not want to take values from the regular images.
      // For lack of a better decision, we use the regular images for now. 
      //    If the image is a video, we can't make use of regular images so he
      // or she must provide a hithumb and inform us about it through the sizes
      // list of as an explicit value. 
      if( !item.hithumb && item.hithumb!=''
       && ( item.extn=='.mp4' ||
            item.thumbshape[0]*item.shape[1]!=item.thumbshape[1]*item.shape[0] ) )
          for(item.hithumb=new Array(sss[sind].hithind),k=j=0;j<ss.length;j++) 
            if(ss[j].type=='hithumb') 
              item.hithumb[k++] = { scale:ss[j].scale , suffix:ss[j].suffix } ;

      // rawshape
      for(r1=k=0;k<ss.length;k++) if(ss[k].type=='raw')
        r1 = ss[k].scale / ss[0].scale ; 
      if(r1&&!item.rawshape) for(item.rawshape=[0,0],k=0;k<2;k++) 
        item.rawshape[k] = Math.floor(0.5+item.shape[k]*r1) ; 

      geturialias(item,'imagedir',aliases) ; 
      item.visited = item.overlaid = 0 ; 
    }
  } 

  for(i=0;i<links.length;i++) geturialias(links[i],'href',aliases) ; 
  geturialias(pixp,'href',aliases) ; 
  geturialias(ic,'href',aliases) ; 
  geturialias(imagedir,'href',aliases) ; 

  for(sno=0;sno<sect.length;sno++)
    for(item=sect[sno],ll=item.list,i=0;i<ll.length;i++) if(ll[i].extn=='.mp4') 
  { if(item.layout) alert(ll[i].filename+'.mp4 is illegally inside a layout') ;
    if(ll[i].overlay) alert(ll[i].filename+'.mp4 has an illegal overlay') ;
  }

  // at this point, the thumb/hithumb entries in sizes are no longer needed
  // ** it would be nice, at some time, to remove 'raw' too, and to remove the
  // ** filename field from items
  for(sno=0;sno<sss.length;sno++)
  { ss = sss[sno].sizes ;
    for(j=i=0;i<ss.length;i++) if(ss[i].type!='thumb'&&ss[i].type!='hithumb')
      ss[j++] = ss[i] ; 
    ss.length = j ; 
  }

  if(baseuri) // this is an obligatory parameter in practice
  { if(baseuri.substr(0,5)=='http:'&&document.URL.substr(0,6)=='https:')
      baseuri = 'https://www.routemaster.app/resources/fileserver.php?' + 
                baseuri ; 
    pixp = reluri(baseuri,pixp) ;
    for(sno=0;sno<sect.length;sno++)
    { item = sect[sno] ; 
      if(item.gps) for(j=0;j<item.gps.length;j++) 
        item.gps[j] = reluri(baseuri,item.gps[j]) ; 
      for(ll=item.list,i=0;i<ll.length;i++) 
      { ll[i].filename = reluri(baseuri,ll[i].filename) ; 
        ll[i].imagedir = reluri(baseuri,ll[i].imagedir) ;
      }
    }
  }

  for(i=0;i<links.length;i++) if(links[i].href) 
    links[i].href = reluri(baseuri,links[i].href) ; 

  for(i=0;i<sty.length;i++) if(s=sty[i].style.farg)
  { s = s.split(' ') ; 
    if(s.length==1) { sty[i].style.farg = s[0] ; continue ; }
    for(k=j=0;j+1<s.length;j+=2) 
    { if(s[j].substring(0,4)=='url(')
      { if(s[j].charAt(s[j].length-1)!=')') alert('illegal farg url '+s[j]) ; 
        s[j] = s[j].substring(4,s[j].length-1) ; 
      }
      if(s[j].charAt(0)=='"'||s[j].charAt(0)=="'")
      { if(s[j].charAt(s[j].length-1)!=s[j].charAt(0)) 
          alert('badly formatted farg url '+s[j]) ; 
        s[j] = s[j].substring(1,s[j].length-1) ; 
      }
      s[k++] = { url:'url("'+reluri(baseuri,s[j])+'") ' , scale:s[j+1] } ; 
    }
    sty[i].style.farg = s.slice(0,k) ; 
  }

  return { title:title , icon:ic , rawsect:sect , sect:null , style:sty , 
           links:links , gallery:pixp , origin:orp , caption:caption , 
           vis:null , cats:cats , maxthumb:null , alias:aliases } ;

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

  function geturialias(item,field,aliases)
  { var i ;
    if(!item||!item[field]) return ;
    if(field!='gps') item[field] = dourialias(item[field],aliases) ; 
    else for(i=0;i<item[field].length;i++)
      item[field][i] = dourialias(item[field][i],aliases) ; 

    function dourialias(item,aliases)
    { var k,aliasno,uri ;
      k = item.indexOf(',') ; 
      if(k<=0) return item ; 
      alias = item.substring(0,k) ;
      uri = item.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)!='/' ) return aliases[k].uri + '/' + uri ; 
      else return aliases[k].uri + uri ; 
    }
  }
  /* ------------------------------------------------------------------------ */

  function genobject(node,l)
  { var i,j,v,r={},ind ;
    for(i=0;i<l.length&&l[i];i++) 
    { v = node.getAttribute(l[i]) ; 
      if((l[i]=='scale'||l[i]=='fontsize')&&v!=null&&v!='') v = parseFloat(v) ; 
      else if(l[i]=='gps'&&v!=null) v = v.split(' ') ; 
      r[l[i]] = v ; 
    }
    return r ;
  }
  /* ------------------------------------------------------------------------ */

  function genimgobject(node,fields)
  { var img,i,kids=node.childNodes,doc ;
    if(!node) 
    { for(img={},i=0;i<fields.length;i++) img[fields[i]] = null ; return ; }
    img = genobject(node,fields) ;
    img.texttitle = img.title ; 
    for(i=0;i<kids.length;i++) if(kids[i].textContent) 
    { if(kids[i].nodeName=='caption') img.caption = kids[i].textContent ;
      else if(kids[i].nodeName=='title') 
      { img.title = kids[i].textContent ;
// https://stackoverflow.com/questions/822452/...
//    ...strip-html-tags-from-text-using-plain-javascript/47140708#47140708
        doc = new DOMParser().parseFromString(img.title,'text/html') ;
        img.texttitle = doc.body.textContent ;
      }
    }
    return img ; 
  }
  /* ------------------------------------------------------------------------ */

  function getxmlsize(item,aliases)
  { var i,j,w,sfx ; 
    if(!item) return null ;
    i = item.indexOf('[') ; 
    if(i>=0)
    { for(j=0;j<i&&item.charAt(j)==' ';j++) ; 
      sfx = item.substring(j,i) ; 
      // advance i beyond spaces
      for(i++;i<item.length&&item.charAt(i)==' ';i++) ;
      for(j=i;j<item.length&&item.charAt(j)!=','&&item.charAt(j)!=' '
                           &&item.charAt(j)!=']';j++) ;
      if(j==item.length) { alert("badly formed shape: "+item) ; throw '' ; }
      w = item.substring(i,j) ;
      if(item.charAt(j)==']') return [ parseFloat(w) , null, sfx ] ;
      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 [ parseInt(w) , parseInt(item.substring(i+1,j)) , sfx ] ;
    }
    if(aliases) for(i=0;i<aliases.length;i++)
      if(item==aliases[i].name) return aliases[i].shape ;
    alert("shape "+item+" not defined") ; 
    throw '' ; 
  }
  function getxmlsizes(item)
  { var i,ilen,sfx,seq,sizeno,ss ; 
    if(!item) return null ;
    seq = item.split(" ") ; 
    ss = new Array(seq.length) ; 
    for(sizeno=0;sizeno<seq.length;sizeno++) 
      if(item=seq[sizeno],ilen=item.length,ilen>0)
    { i = item.indexOf('[') ; 
      if(i<0||item.charAt(ilen-1)!=']') 
      { alert(item+' is a faulty hithumb shape') ; throw '' ; }
      ss[sizeno] = { scale:  parseFloat(item.substring(i+1,ilen-1)) ,
                     suffix: item.substring(0,i) } ; 
    }
    for(ilen=i=0;i<seq.length;i++) if(ss[i]) ss[ilen++] = ss[i] ; 
    return ss.slice(0,ilen) ; 
  }
}
/* -------------------------------------------------------------------------- */
/*page*/
/* ----------------------------- image functions ---------------------------- */

function imgshape(item,sizeno,ss)
{ if(sizeno<0) return item.thumbshape ; 
  if(!ss) ss = item.sizes ; 
  if(ss[sizeno].type=='raw') return item.rawshape ; 
  else if(item.extn=='.mp4'&&sizeno>=0) 
    return [ item.shape[0] , item.shape[1] ] ;
  return [ Math.floor(0.5+item.shape[0]*ss[sizeno].scale/ss[0].scale) ,
           Math.floor(0.5+item.shape[1]*ss[sizeno].scale/ss[0].scale) ] ;
}
/* -------------------------------------------------------------------------- */
/*page*/
/* ----- sparepix finds the margins left if item is displayed at sizeno -
         these are the horizontal margins according as the image is 
         displayed landscape or portrait                                 ---- */

function sparepix(item,sizeno)
{ var shape = imgshape(item,sizeno) ; 
  if(!shape) 
  { alert('Somehow an illegal size was passed to sparepix') ; return -1000 ; }
  var r , w = shape[0] , h = shape[1] + gethincr(item,sizeno,shape) ;

  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 ;
}
function gethincr(item,sizeno,shape)
{ var r,sizes=item.sizes ; 
  if(sizeno<0) r = shape[3] ; else r = sizes[sizeno].fontsize ;
  r = Math.floor(0.5+1.25*r) + 2 ;
  if(item.caption||item.overlay) r += Math.floor(0.5+r) + 2 ;
  return r ;
}
/* ---------------------- construct the jpg path name ----------------------- */

function jpg(item,sizeno,ovl)
{ var sfx , extn = ovl?'.png':(item.extn=='.mp4'?'.jpg':item.extn) ;
  if(sizeno<0) sfx = item.thumbshape[2] ; else sfx = item.sizes[sizeno].suffix ; 
  return item.filename + sfx + extn ; 
}
/* ------- getsize finds the largest image size which fits the screen ------- */

function getsize(item,sizes) 
{ var i,ibest,spare ;
  if(!sizes) sizes=item.sizes ;

  if(sizes.length==0) return -1 ; 
  for(ibest=-1,i=0;i<sizes.length;i++) if(i<0||!sizes[i].type)
  { spare = sparepix(item,i) ; // spare pix for landscape/portrait
    if(spare[0]>=0||spare[1]>=0) 
      if(ibest<0||sizes[i].scale>sizes[ibest].scale) ibest = i ; 
  }
  return ibest ;
}
function getmp4shape(item)
{ var qh = window.innerHeight - gethincr(item,0) ;
  var ql = Math.min(window.innerWidth/item.shape[0],(qh-50)/item.shape[1]) ; 
  var qp = Math.min((window.innerWidth-50)/item.shape[0],qh/item.shape[1]) ; 
  if(qp>ql) qh = qp ; else qh = ql ; 
  if(qh>1&&!queryfullscreen()) qh = 1 ; 
  return [ qh*item.shape[0] , qh*item.shape[1] , qp>ql ] ; 
}
/* -------------------------------------------------------------------------- */
/*page*/
/* --------------------------------- genimage ------------------------------- */

function genimage(item,sizeno,parms) 
{ var sizes , qres , imgsize , q , r , debug = 1 , ovl ; 
  var hno = { i:-1 , scl:1 } , ovl = parms&&parms.dooverlay , width = null ;
  var ret = { img:null , src:null , shape:null , 
              lf:null , res:1 , resno:null , sty:null } ;
  if(parms&&parms.loadfunc) ret.lf = parms.loadfunc ; 
  if(parms&&parms.sizes) sizes = parms.sizes ; else sizes = item.sizes ; 

  function div(x) { return Math.abs(Math.log(x)) ; } // divergence

  // --- find image dimensions
  if(parms&&parms.width)
  { width = parms.width ; 
    if(sizeno&&sizeno<0)
      ret.shape = [ width , (item.thumbshape[1]*width) / item.thumbshape[0] ] ;
    else
    { sizeno = 0 ; 
      ret.shape = [ width , (item.shape[1]*width) / item.shape[0] ] ;
    }
  }
  else
  { if((!sizeno)&&sizeno!=0&&sizes.length) sizeno = getsize(item,sizes) ;
    ret.shape = imgshape(item,sizeno,sizes) ;
    if(!ret.shape) 
    { alert('Somehow an illegal size was passed to genimage') ; return null ; }
  }

  // --- choose preferred resolution qres: 0<=qres<=1
  if(debug||!parms||(!parms.resno&&parms.resno!=0)) 
  { if(!genimage.res&&genimage.res!=0) genimage.res = 0.2 ;
    if(genimage.res>=1) qres = 1000000 ; 
    else if(genimage.res<=0) qres = 1 ; 
    else 
    { imgsize = ret.shape[0] * ret.shape[1] / 1000000 ; 
      q = Math.atanh(genimage.res) / imgsize ; 
      qres = 1 + ((window.devicePixelRatio||1)-1) * Math.tanh(q) ;
    }
  }

  // for raw/video choose appropriate src 
  if(item.extn=='.mp4'&&sizeno>=0) ret.src = item.filename + '.jpg' ; 
  else if(sizeno>0&&sizes[sizeno].type=='raw') ret.src = jpg(item,sizeno,ovl) ;

  // --- src for a thumbnail choosing from custom hithumbs 
  else if(sizeno<0&&(h=item.hithumb))
  { if(width) q = item.thumbshape[0] / width ; else q = 1 ; 
    if(parms&&(parms.resno||parms.resno==0))
    { if(parms.resno<0) hno = { i:-1 , scl:q } ; 
      else hno = { i:parms.resno , scl:q*h[parms.resno].scale } ;
    }
    else for(hno.scl=q,i=0;i<h.length;i++)
    { r = q * h[i].scale ; 
      if(div(r/qres)<div(hno.scl/qres)) hno = { i:i , scl:r } ; 
    }
    if(hno.i<0) ret.src = jpg(item,-1) ; 
    else ret.src = item.filename + h[hno.i].suffix + 
                                   (item.extn=='.mp4'?'.jpg':item.extn) ; 
  }

  // --- src for a thumbnail with no hithumbs when we can't use regular images
  else if( sizeno<0 && (item.extn=='.mp4'||
           item.thumbshape[0]*item.shape[1]!=item.thumbshape[1]*item.shape[0]) )
    ret.src = jpg(item,-1,ovl) ; 

  // --- src for a thumbnail when we use regular images
  else if(sizeno<0)
  { if(width) q = item.shape[0] / ( width * sizes[0].scale ) ; 
    else q = item.shape[0] / ( item.thumbshape[0] * sizes[0].scale ) ; 
    if(width) hno.scl = item.thumbshape[0] / width ; 
    if(parms&&(parms.resno||parms.resno==0))
      hno = { i:parms.resno , scl:q*sizes[parms.resno].scale } ;
    else for(i=0;i<sizes.length;i++) if(!sizes[i].type) 
    { r = q * sizes[i].scale ; 
      if(div(r/qres)<div(hno.scl/qres)) hno = { i:i , scl:r } ; 
    }
    ret.src = jpg(item,hno.i,ovl) ;
  }

  // --- or src for a regular image (could merge with preceding code)
  else 
  { if(width) q = item.shape[0] / ( sizes[0].scale * width ) ; 
    else q = 1 / sizes[sizeno].scale ; 
    if(parms&&(parms.resno||parms.resno==0))
      hno = { i:parms.resno , scl:q*sizes[parms.resno].scale } ;
    else for(i=0;i<sizes.length;i++) if(!sizes[i].type)
    { r = q * sizes[i].scale ;
      if(hno.i<0||div(r/qres)<div(hno.scl/qres)) hno = { i:i , scl:r } ; 
    }
    ret.src = jpg(item,hno.i,ovl) ;
  }

  // --- complete return structure to finish
  ret.res = hno.scl ;
  ret.resno = hno.i ; 
  if(ovl||item.extn=='.mp4') ret.sty = "position:absolute;left:0;top:0" ;
  if(debug) 
  { if(qres>10000) qres = '∞' ; else qres = pixscale(qres) ;
    console.log(ret.src.substring(1+ret.src.lastIndexOf('/')) + '[' +
                ret.shape[0] + ',' + ret.shape[1] + ']; res=' +
                hno.scl.toFixed(2) + ' (aiming for ' + qres + ')') ;
  }
  return ret ;
}
function doim(im)
{ im.img = domcreate('img') ;
  im.img.setAttribute('width',im.shape[0]) ; 
  im.img.setAttribute('height',im.shape[1]) ; 
  if(im.lf) { im.img.onload = im.lf ; im.img.onerror = im.lf ; }
  if(im.sty) im.img.style = im.sty ;
  im.img.setAttribute('src',im.src) ; 
  return im ; 
}
/* -------------------------------------------------------------------------- */
/*page*/
/* -------------------- find the list ind with a given name ----------------- */

function findimage(sect,name,serialno)
{ var sno,ind,ll,n,list,starred,i,j,v ; 

  if(!name) // return a random starred image
  { for(n=sno=0;sno<sect.length;sno++) n += sect[sno].list.length ;
    list = new Array(n) ; 
    for(starred=j=sno=0;sno<sect.length;sno++) 
      for(ll=sect[sno].list,i=0;i<ll.length;i++)
        if((v=ll[i].visibility)!='none')
    { if(starred) { if(v=='*') list[j++] = [sno,i] ; }
      else 
      { if(v=='*') { list[0] = [sno,i] ; j = starred = 1 ; }
        else list[j++] = [sno,i] ;
      }
    }
    if(j==0) { alert('no items to display') ; throw '' ; }
    if(typeof serialno=='number'&&serialno>=0&&serialno<1) ; 
    else serialno = Math.random() ; 
    return list[Math.floor(j*serialno)] ;
  }

  if(!serialno) serialno = 0 ; 
  for(sno=0;sno<sect.length;sno++) for(ll=sect[sno].list,i=0;i<ll.length;i++)  
    if(ll[i].name==name&&ll[i].serialno==serialno) return [sno,i] ; 
  return null ;
}
/* -------------------------------------------------------------------------- */
/*page*/
/* -------------------------------------------------------------------------- */

var loads = [] , pending = [] , thislist = null , domparser = null ;

function thumb(p1,p2) 
{ if(typeof p1=='string') return showimg(p1,null,p2,1) ; 
  else return showimg(p1,null,null,1) ;
}
function showimg(p1,p2,p3,thumbopt) 
{ var ind,s,photolist,sh,i,shh,thindw,item,href,sect,a,img,title,q,im ; 
  var domopt=null , width=null , pixref=null; 
  var listno=null,name=null,compression=null,serialno=null,linkuri=null ;

  if(loads.length==0) 
  { alert('No photo list – loadpix() not called') ; throw '' ; }

  if(typeof p1=='string') { name = p1 ; width = p2 ; serialno = p3 ; }
  else if(typeof p1=='number') { listno = p1 ; width = p2 ; }
  else if(typeof p1=='object')
  { listno = p1.listno ; 
    name = p1.name ; 
    width = p1.width ;
    serialno = p1.serialno ; 
    linkuri = p1.linkuri ; 
    domopt = p1.dom ; 
  }

  if(listno!=0&&!listno) listno = loads.length-1 ; 

  if(listno<0||listno>=loads.length)
  { alert('list no out of range: '+listno+'/'+list.length) ; throw '' ; }
  photolist = loads[listno][1] ;

  if(!photolist) 
  { a = null ; 
    if(domopt) a = domcreate('a') ;
    else document.write('<a id=pixpending' + pending.length+'></a>') ; 
    pending.push([name,serialno,listno,width,linkuri,a,thumbopt]) ; 
    return a ; 
  }

  if(null==(ind=findimage(photolist.sect,name,serialno))) 
  { alert('Missing image: '+name+(serialno>0?('('+serialno+')'):'')) ;
    throw '' ;
  }

  href = photolist.gallery ;
  if(name) href += 
    '?image=' + name + (serialno>0?('&serialno='+serialno):'') + '&mode=n' ;

  sect = photolist.sect[ind[0]] ;
  item = sect.list[ind[1]] ;
  if(item.texttitle) title = item.texttitle ;
  else if(sect.title) title = parsetitle(sect)[0] ;
  else title = null ;

  im = genimage(item,thumbopt?-1:null,{width:width}) ; 

  if(domopt)
  { a = domcreate('a',null,'href',linkuri?linkuri:href) ;
    a.setAttribute('imglink',href) ; 
    img = doim(im).img ; 
    img.setAttribute('class','pix') ; 
    if(title) img.setAttribute('title',title) ; 
    a.appendChild(img) ; 
    return a ; 
  }

  a = '<a href="' + (linkuri?linkuri:href) +'" imglink="' + href +'">' + 
      '<img class=pix src="' + im.src +
      '" width=' + im.shape[0] + ' height=' + im.shape[1] ;
  if(title) a += ' title="' + title + '"' ;
  document.write(a+'></a>') ; 
}
/* -------------------------------------------------------------------------- */

function dothumbs(listno)
{ var i,a,img,name,serialno,ind,item,photolist,href,sizeno,sect,width,q ; 

  for(i=0;i<pending.length;i++) if(pending[i][2]==listno)
  { name = pending[i][0] ;
    serialno = pending[i][1] ;
    width = pending[i][3] ;
    sizeno = pending[i][6]?-1:null ;
    photolist = loads[listno][1] ;
    if(null==(ind=findimage(photolist.sect,name,serialno))) 
    { alert('Missing image: '+name+(serialno>0?('('+serialno+')'):'')) ;
      continue ;
    }
    sect = photolist.sect[ind[0]] ;
    item = sect.list[ind[1]] ;
    if(pending[i][5]) a = pending[i][5] ;
    else a = document.getElementById('pixpending'+i) ; 
    if(!a) { alert('Logic error: no element pixpending'+i) ; continue ; }

    href = photolist.gallery ;
    if(name) href += 
      '?image='+name + (serialno>0?('&serialno='+serialno):'') + '&mode=n' ;
    a.setAttribute('href',pending[i][4]?pending[i][4]:href) ; 
    a.setAttribute('imglink',href) ; 

    img = doim(genimage(item,sizeno,{width:width})).img ; 
    img.setAttribute('class','pix') ; 
    if(item.texttitle) img.setAttribute('title',item.texttitle) ; 
    else if(sect.title) img.setAttribute('title',parsetitle(sect)[0]) ; 
    a.appendChild(img) ; 
  }
}
/* -------------------------------------------------------------------------- */

function loadpix(xmlfl)
{ var i , xmldoc , xhttp = new XMLHttpRequest() ;
  if(!domparser) domparser = new DOMParser() ;
  for(i=0;i<loads.length;i++) if(loads[i][0]==xmlfl) return i ;
  xhttp.onreadystatechange = function() 
  { if(xhttp.readyState==4) 
    { if(xhttp.status==200)
      { xmldoc = 
          domparser.parseFromString(xhttp.responseText,"application/xml") ;
        for(i=0;i<loads.length&&loads[i][0]!=xmlfl;i++) ;
        if(i==loads.length) { alert('logic error') ; throw '' ; }
        loads[i][1] = getphotolist(xmldoc,xmlfl) ; 
        selcat(loads[i][1]) ; 
        dothumbs(i) ;
      }
      else alert("Unable to read "+xmlfl+": error code "+xhttp.status) ;
    }
  }
  loads.push([xmlfl,null]) ;
  xhttp.open("GET",xmlfl,true) ;
  xhttp.send() ;
  return loads.length-1 ; 
}
/* -------------------------------------------------------------------------- */

function genpic(element,item,title,laction,litem,retaction,raction,ritem,
                infowords,helpdiv,direction,infoflag,style) 
{ var i,a,udiv,ddiv,llink,rlink,fetchitem,drawparms,itemparms,name,d ;
  if(genpic.drawparms==undefined) genpic.drawparms = null ; 
  if(genpic.itemparms==undefined) genpic.itemparms = null ; 

  if(!element) 
  { genpic.itemparms = genpic.drawparms = null ; 
    if(genpic.element) for(element=genpic.element;element.firstChild;)
      element.removeChild(element.firstChild) ;
    return ; 
  }
  if(element=='uncaption') { uncaption() ; return ; }
  else if(element=='resize') { resize() ; return ; }
  else if(element=='hi-res') { genmenu('del') ; resize(2) ; return ; }
  else if(element=='enlarge') { genmenu('del') ; resize(1) ; return ; }
  else if(element=='reduce') { genmenu('del') ; resize(-1) ; return ; }
  else if(element=='lo-res') { genmenu('del') ; resize(-2) ; return ; }
  else if(element=='spacebar')
  { if(!genpic.itemparms||!genpic.drawparms||!genpic.itemparms.item) return 0 ; 
    item = genpic.itemparms.item ;
    if(!item.extn||item.extn!='.mp4') return 0 ; 
    var img = genpic.drawparms.img ;
    if(img.currentTime>0&&!img.paused&&!img.ended) img.pause() ; 
    else img.play() ; 
    return 1 ;
  }    
  genpic.element = element ; 
  genpic.style = defstyle ; 
  if(style) for(i in defstyle) if(style[i]) genpic.style[i] = style[i] ; 

  if(itemparms) alert('calling genpic when ' + itemparms.item.name +
                      ' is being displayed') ; 
  drawparms = genpic.drawparms ;
  var sizeno = (item.extn=='.mp4'?0:getsize(item)) ;
  item.visited = 1 ; 

  // decide which image if any to prefetch 
  fetchitem = null ; 
  if(direction<0) { if(litem) fetchitem = litem ; } 
  else if(ritem) fetchitem = ritem ; 

  genpic.itemparms = itemparms = 
    { item:item , sizeno:sizeno , holdsize:0 , fetchitem:fetchitem , res:1 } ;

  while(element.firstChild) element.removeChild(element.firstChild) ;

  /* -------------------- create a cell for navigation icon ----------------- */

  function navcell(action,item,dir)
  { var a,astyle='',s='center' ;
    if(dir==-1) s = 'left' ; else if (dir==1) s = 'right' ;
    s = "display:inline-block;position:absolute;text-align:" + s ; 
    s += ((dir==1||dir==-1)?';font-size:20px;':';font-size:16px;') ;
    s += 'vertical-align:middle;width:16px;height:16px;' ;
    if(dir==2||dir==-2) s += 'left:16px;top:' + 8*(dir+2) + 'px' ;
    else s += 'top:16px;left:' + 16*(dir+1) + 'px' ;
    var div = domcreate('div',null,"style",s)

    if(!action) return div ; 
    div.style.cursor = 'pointer' ;

    if(dir>0) s = "next: " + item.name + " [\u2192 key]" ; 
    else if(dir==0) s = "back to " + item + " [\u21b5 key]" ; 
    else s = "prev: " + item.name + " [\u2190 key]" ;
    a = domcreate('span',null,"title",s) ; 

    if(dir==0||item.visited) astyle = 'color:' + genpic.style.visited ;
    else astyle = 'color:' + genpic.style.link ;
    a.setAttribute("style",astyle) ; 

    if(dir>0) s = '>' ; else if(dir==0) s = '\u21b5' ; else s = '<' ;
    domadd(a,s) ; 
    div.appendChild(a) ; 
    div.addEventListener('click',action) ; 
    return div ; 
  }
  /* ------------------------------------------------------------------------ */

  if(infoflag) name = infowords.notes ; else name = infowords.origin ; 
  d = domcreate('div',null,'style',
                      'position:fixed;top:0;left:0;width:50px;height:50px;' +
                      'z-index:1;background:'+genpic.style.bg) ; 
  d.appendChild(navcell(laction,litem,-1)) ;  // '<'
  d.appendChild(udiv=navcell(null,null,-2)) ; // enlarge
  d.appendChild(navcell(raction,ritem,1)) ;   // '>'
  d.appendChild(ddiv=navcell(null,null,2)) ;  // reduce
  d.appendChild(navcell(retaction,name,0)) ;  // return
  element.appendChild(d) ;  

  genpic.drawparms = drawparms = 
              { headh:     null, 
                headf:     null, 
                caph:      null, 
                capf:      null, 
                enlink:    udiv,
                redlink:   ddiv,
                img:       null,
                ovl:       null,
                oving:     0,
                portrait:  null,
                navdiv:    d,
                bgdiv:     null,
                maindiv:   domcreate('div'), 
                headdiv:   null,
                imgdiv:    domcreate('div',null,'style','position:relative'),
                capdiv:    null,  
                capspan:   null,  
                capboffs:  null,
                capleft:   null,
                captionbox:null
              } ;

  if(genpic.style.farg) drawparms.bgdiv = domcreate('div') ;

  if(item.title)
  { drawparms.headdiv = domcreate('div') ;
    drawparms.headdiv.innerHTML = item.title ; 
  }

  a = setimg(item,sizeno,{loadfunc:fetchitem})
  itemparms.res = a.r ; 
  itemparms.resno = a.resno ; 

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

  element.appendChild(drawparms.maindiv) ; 
  genmenu(element,menufactory(item,title,infowords,
                                 retaction,helpdiv,infoflag),genpic.style) ;
  return ;    // end of genpic

  function menufactory(item,title,infowords,retaction,helpdiv,infoflag) 
  { return function() { return photoinfodiv(item,title,infowords,
                                            retaction,helpdiv,infoflag) ; }
  } ;
  /* -------------- reposition image in response to a window resize --------- */

  function resize(k,kk) 
  { var itemparms = genpic.itemparms , drawparms = genpic.drawparms ; 
    if(!itemparms) return ; 
    var item = itemparms.item , sizes = item.sizes , s , d , f ;
    var sizeno = itemparms.sizeno ;

    // https://stackoverflow.com/questions/2956966/
    //                     javascript-telling-setinterval-to-only-fire-x-amount-of-times
    function setrepeater(f,interval,ntimes) 
    { var timer , alarm = function () 
      { ntimes -= 1 ; f(ntimes) ; if(ntimes==0) window.clearInterval(timer) ; }
      timer = window.setInterval(alarm,interval) ;
    }
    function zonkfactory(d)
    { return function(ntimes) 
      { if(ntimes) d.style.opacity = ntimes / 5 ; else d.parentNode.removeChild(d) ; }
    }

    if(item.extn=='.mp4'&&sizeno>=0) 
    { shape = getmp4shape(item) ; 
      drawparms.portrait = shape[2] ; 
      shape = setimg(item,drawparms.img) ;
      itemparms.res = shape.r ;
      itemparms.resno = shape.resno ;
      return ; 
    }

    if(k=='regular'||k>0||k<0)
    { if(k==2||k==-2)
      { if((k=reres(item,itemparms.sizeno,itemparms.resno,k))==null) return ;
        uncaption() ; 
        shape = setimg(item,itemparms.sizeno,{resno:k}) ; 
        s = pixscale(itemparms.res) + 'x→' + pixscale(shape.r) + 'x' ;      
        d = domcreate('div',s,'style','position:absolute;'+
                       'height:20px;width:100px;top:0;background:white')
        domadd(drawparms.imgdiv,d) ; 
        setrepeater(zonkfactory(d),100,5) ; 
        itemparms.sizeno = shape.s ; 
        itemparms.res = shape.r ; 
        itemparms.resno = k ; 
        itemparms.holdsize = 1 ;  
        return ; 
      }
      else if(k=='regular') k = kk ; 
      else if((k=rescale(item,sizeno,k))==null) return ;
      uncaption() ; 
      shape = setimg(item,k) ; // setimg draws img + navcells
      itemparms.sizeno = shape.s ; 
      itemparms.res = shape.r ; 
      itemparms.resno = shape.resno ; 
      itemparms.holdsize = 1 ;  
      return ; 
    }
    var flag=0 , spare , newsizeno = getsize(item) ;

    // holdsize indicates that an enlargement/reduction has been requested, and
    // shouldn't be made ineffective when the window is resized
    //    if holdsize isn't set and a new size is better, call setimg & return
    if(itemparms.holdsize==0&&newsizeno!=sizeno) 
    { shape = setimg(item,newsizeno,{loadfunc:itemparms.fetchitem}) ;
      itemparms.res = shape.r ; 
      itemparms.resno = shape.resno ; 
      itemparms.sizeno = newsizeno ;
      return ; 
    } 

    // if we get here, we have to retain size sizeno
    if(newsizeno==sizeno) itemparms.holdsize = 0 ; // because this is voluntary
    sizeno = newsizeno ; 

    // redisplay if portrait<->landscape makes a fit possible, else redraw
    if(!(shape=imgshape(item,sizeno))) 
    { alert('Somehow an illegal size was passed to resize') ; throw '' ; }
    spare = sparepix(item,sizeno) ;
    if(drawparms.portrait!=(spare[0]<spare[1])&&(spare[0]<0)!=(spare[1]<0))
      drawparms.portrait = 1-drawparms.portrait ;
    setimgpos(shape,item.caption) ;
  }
  /* ----------- rescale finds the next larger/next smaller image ----------- */

  function rescale(item,sizeno,dir)
  { var i,u,ind,size,cursize,bestsize,sizes=item.sizes ;
    if(item.extn=='.mp4'&&sizeno>=0) return null ;
    function imgsize(item,sizeno)
    { var u = imgshape(item,sizeno) ; 
      if(u) return u[0] + u[1] ; else return null ; 
    }

    cursize = imgsize(item,sizeno) ;
    for(bestsize=ind=null,i=0;i<sizes.length;i++) if(i!=sizeno)
    { if(!(size=imgsize(item,i))) continue ; 
      if(dir<0)
      { if(size<cursize&&(bestsize==null||size>bestsize))
        { ind = i ; bestsize = size ; }
      }
      else if(size>cursize&&(bestsize==null||size<bestsize))
      { ind = i ; bestsize = size ; }
    }
    return ind ;
  }
  /* ------------- reres finds the next higher/lower resolution ------------- */
  
  function reres(item,sizeno,resno,dir)
  { var r=window.devicePixelRatio , res , bestres , sizes=item.sizes ;
    var origres = sizes[resno].scale / sizes[sizeno].scale ;
    if(!r) r = 1 ;

    for(bestres=ind=null,i=0;i<sizes.length;i++) if(i!=resno)
    { res = sizes[i].scale / sizes[sizeno].scale ;
      if(dir<0)
      { if(res<origres&&((!bestres&&res>=1)||res>bestres))
        { ind = i ; bestres = res ; } 
      }
      else if(res>origres&&((!bestres&&res<=r)||res<bestres)) 
      { ind = i ; bestres = res ; } 
    }
    return ind ;
  }
  /* ------ setimgpos sets the image position according to window size ------ */

  // sets styles of parms.maindiv, parms.headdiv, parms.capdiv, parms.imgdiv
  // and sets event listener of parms.capspan; uses drawparms.captionbox
  //    basically it does the work for a new window size when this does not 
  // require a change of image size

  function setimgpos(shape,caption)
  { var itemparms = genpic.itemparms , drawparms = genpic.drawparms ; 
    var imgw=shape[0],imgh=shape[1],x,y,s,overflowx,overflowy,r,d ;
    var bgwidtop,bgwidside,style=genpic.style ;
    var pad = drawparms.headh + (drawparms.caph?drawparms.caph+4:0) ;
    // set space to the amount of room we've got for the image div
    var space = [ window.innerWidth , window.innerHeight-pad ] ;
    if(drawparms.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 inner 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 - drawparms.headh;
    if(drawparms.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 ;

    if(drawparms.bgdiv) 
    { bgwidtop = Math.floor(y/2) ; 
      if(bgwidtop>y-4) bgwidtop = y-4 ; 
      if(bgwidtop>72) bgwidtop = 72 ; else if(bgwidtop<0) bgwidtop = 0 ; 
      bgwidside = Math.floor(x/2) ; 
      if(bgwidside>x-4) bgwidside = x-4 ; 
      if(bgwidside>72) bgwidside = 72 ; else if(bgwidside<0) bgwidside = 0 ; 
      s = 'position:absolute;left:' + bgwidside + 'px;width:' +
          (window.innerWidth-2*bgwidside) + 'px;top:'+bgwidtop + 'px;height:' +
          (window.innerHeight-2*bgwidtop) + 'px;background:' + style.bg ;
      drawparms.bgdiv.setAttribute('style',s) ; 
    }

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

    if(drawparms.headh)
    { s = 'position:absolute;left:0;width:' + (window.innerWidth-x) +
          'px;top:2;height:' + (drawparms.headh-2) + 'px;overflow:hidden;' + 
          'text-overflow:ellipsis;white-space:nowrap;font-size:' +
           drawparms.headf + 'px;color:' + style.fg ;
      drawparms.headdiv.setAttribute('style',s) ; 
    }
    drawparms.capboffs = pad+imgh+y ;
    drawparms.capleft = x ; 

    function expandcaption()
    { if(drawparms.captionbox) return ; 
      if(drawparms.capspan.getBoundingClientRect().right<window.innerWidth) 
        return ;
      drawparms.captionbox = domcreate('div',null,'style',
           'position:fixed;color:' + style.mfg + ';background:'+ 
           style.mbg + ';bottom:' + (window.innerHeight-drawparms.capboffs) +
           ';left:' + drawparms.capleft+';padding:8;opacity:0.9' ) ;
      drawparms.captionbox.innerHTML = caption ;
      genpic.element.appendChild(drawparms.captionbox) ; 
      setTimeout(uncaption,1500+caption.length*20) ;
    }

    if(drawparms.capdiv!=null)
    { s = 'position:absolute;left:0;width:' + (window.innerWidth-x) +
          'px;bottom:0;height:' + drawparms.caph+'px;overflow:hidden;color:'+
          style.mg+';font-size:'+drawparms.capf+'px;text-overflow:' +
          'ellipsis;white-space:nowrap';
      drawparms.capdiv.setAttribute('style',s) ; 
      if(caption) 
        drawparms.capspan.addEventListener('mouseenter',expandcaption) ;
    }

    if(imgw<shape[0]) overflowx = 'scroll' ; else overflowx = 'hidden' ; 
    if(imgh<shape[1]) overflowy = 'scroll' ; else overflowy = 'hidden' ; 
    s = 'position:absolute;z-index:2;left:0;width:' + imgw + 'px;top:' +
        drawparms.headh + 'px;' +'height:' + imgh + 'px;overflow-x:' +
        overflowx + ';overflow-y:'+overflowy ;
    drawparms.imgdiv.setAttribute('style',s) ; 
  }
  /* --- adjust element attributes in accordance with a change of img size -- */

  // creates enlarge/reduce cells
  // sets drawparms.headh, drawparms.headf, drawparms.capf, drawparms,caph, 
  //    drawparms.portrait
  // calls genimage, assigns result to drawparms.img and appends it to 
  //    drawparms.imgdiv
  // sets drawparms.capf, drawparms.caph
  // calls setimgpos

  function setimg(item,sizeno,parms)
  { var itemparms = genpic.itemparms , drawparms = genpic.drawparms ; 
    var font,spare,shape,img=null,ovl,s,i,capsize,astyle,resizer,a,motto,res ; 
    var fetchitem = (parms&&parms.loadfunc)?parms.loadfunc : null ; 

    if(typeof(sizeno)=='object') // this is when resizing a video?
    { if(item.extn!='.mp4') { alert("logic error") ; throw '' ; }
      else { img = sizeno ; sizeno = 0 ; } 
    }
 
    makecaption(item,sizeno) ;

    if(item.extn=='.mp4'&&sizeno>=0) 
    { shape = getmp4shape(item) ; drawparms.portrait = shape[2] ; }
    else 
    { /* ------------------ create/reset enlarge/reduce icon ---------------- */
      for(dir=-1;dir<=1;dir+=2)
      { if(dir<0) div = drawparms.redlink ; else div = drawparms.enlink ;
        while(div.firstChild) div.removeChild(div.firstChild) ;
        resizer = dir<0 ? function(){resize(-1)} : function(){resize(1)} ;

        if(rescale(item,sizeno,dir)!=null) 
        { astyle = 'font-weight:normal;color:' + genpic.style.visited ;
          a = domcreate('span',(dir<0?'\u2296':'\u2295'),"style",astyle) ; 
          motto = dir<0 ? "reduce [\u2193 key]" : "enlarge [\u2191 key]" ;
          a.setAttribute("title",motto) ; 
          div.appendChild(a) ; 
          div.addEventListener('click',resizer) ;
          div.style.cursor = 'pointer' ;
        }
        else
        { div.removeEventListener('click',resizer) ; 
          div.style.cursor = 'default' ;
        }
      }
      /* -------------------------------------------------------------------- */

      spare = sparepix(item,sizeno) ;
      shape = imgshape(item,sizeno) ;
      drawparms.portrait = spare[0]<spare[1] ;
    }
    if(!shape) 
    { alert('Somehow an illegal size was passed to setimg') ; throw '' ; }
    if(sizeno<0) font = item.thumbshape[3] ; 
    else font = item.sizes[sizeno].fontsize ;
    if(!font) 
      alert("Somehow "+item.name+' has a missing fontsize ('+sizeno+')') ;
    capsize = Math.floor(0.5+0.8*font) ;
    if(item.title) drawparms.headh = 2 + Math.floor(0.5+1.25*font) ;
    else drawparms.headh = 0 ; 
    drawparms.headf = Math.floor(0.5+font) ;
    if(item.caption) { drawparms.capf = capsize ; drawparms.caph = font ; }
    else drawparms.capf = drawparms.caph = 0 ; 

    // genimage returns a structure { img,src,shape,lf,res,resno,sty } ;

    // create the image for the new size
    function loadfactory(fetchitem) 
    { return function() { doim(genimage(fetchitem)) ; } ; }
    if(fetchitem) a = loadfactory(fetchitem) ; else a = null ;
    resno = null ; 
    if(img)
    { img.setAttribute('width',shape[0]) ; 
      img.setAttribute('height',shape[1]) ; 
    }
    else if(item.extn=='.mp4'&&sizeno>=0) 
    { img = domcreate('video') ; 
      img.setAttribute('width',shape[0]) ; 
      img.setAttribute('height',shape[1]) ; 
      img.onload = img.onerror = a ;
      img.poster = item.filename + '.jpg' ;
      img.controls = true ; 
      img.preload = 'auto' ; 
      img.src = item.filename + '.mp4' ;
      res = 1 ; 
    }
    else 
    { if(parms&&(parms.resno||parms.resno==0)) res = parms.resno ; 
      else res = null ; 
      img = doim(genimage(item,sizeno,{loadfunc:a,resno:res})) ; 
      res = img.res ; 
      resno = img.resno ; 
      img = img.img ; 
    }
    if(!drawparms.img) drawparms.imgdiv.appendChild(img) ;
    else drawparms.imgdiv.replaceChild(img,drawparms.img) ;
    drawparms.img = img ; 

    if(item.overlay&&drawparms.oving) 
      if(drawparms.ovl.parentNode==drawparms.imgdiv)
        drawparms.imgdiv.removeChild(drawparms.ovl) ; 

    if(item.overlay&&sizeno>=0)
    { drawparms.ovl = doim(genimage(item,sizeno,{dooverlay:1})).img ; 
      if(drawparms.oving) drawparms.imgdiv.appendChild(drawparms.ovl) ;
      drawparms.capf = capsize ;
      drawparms.caph = font ; 
    }

    setimgpos(shape,item.caption) ;
    return { s:sizeno , r:res , resno:resno } ;

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

    function makecaption(item,sizeno)
    { var span,drawparms=genpic.drawparms,div,s ;
      if((!item.caption)&&(sizeno<0||!item.overlay))
      { if(drawparms.capdiv) 
        { drawparms.maindiv.removeChild(drawparms.capdiv) ; uncaption() ; }
        drawparms.capdiv = drawparms.capspan = null ; 
        return ; 
      }

      div = domcreate('div') ; 
      if(item.overlay&&sizeno>=0) 
      { if(drawparms.oving) s = 'Remove ' ; else s = 'Add ' ; 
        span = domcreate('span',s+item.overlay+'.','style',
                         'cursor:pointer;color:'+
                      (item.overlaid?genpic.style.visited:genpic.style.link)) ; 
        div.appendChild(span) ; 
        if(item.caption) div.appendChild(document.createTextNode(' ')) ; 
        span.onclick = function()
        { var s ;
          if(!drawparms.oving) 
          { s = 'Remove ' ; drawparms.imgdiv.appendChild(drawparms.ovl) ; }
          else { s = 'Add ' ; drawparms.imgdiv.removeChild(drawparms.ovl) ; }
          while(this.childNodes.length>0) this.removeChild(this.firstChild) ; 
          this.appendChild(document.createTextNode(s+item.overlay+'.')) ; 
          drawparms.oving = 1 - drawparms.oving ;
          if(!item.overlaid) 
          { this.style.color = genpic.style.visited ; item.overlaid = 1 ; }
        }
      }

      if(item.caption) 
      { span = domcreate('span',null,'style','background:'+genpic.style.bg) ;
        span.innerHTML = item.caption ; 
        div.appendChild(span) ; 
        drawparms.capspan = span ;
      }
      if(drawparms.capdiv) 
        drawparms.maindiv.replaceChild(div,drawparms.capdiv) ; 
      else drawparms.maindiv.appendChild(div) ;
      drawparms.capdiv = div ; 
    }
  }
  /* ------------------------------------------------------------------------ */

  function uncaption()
  { if(genpic.drawparms&&genpic.drawparms.captionbox) 
    { genpic.element.removeChild(genpic.drawparms.captionbox) ; 
      genpic.drawparms.captionbox = null ; 
    }
  }
  /* ------------------------------------------------------------------------ */

  function photoinfodiv(item,title,infowords,origfunc,helpdiv,notesopt)
  { var div = domcreate('div') , s=null , a ; 
    var  d = domcreate('div',null,'style','border-bottom:solid 1px ' + 
                             genpic.style.mfg +';color:' + genpic.style.mfg +
                             ';padding-bottom:3px;margin-bottom:3px') ; 

    var fsoffer = offerfullscreen(infowords,genpic.style.mlink) ;
    if(fsoffer) d.appendChild(fsoffer) ;

    if(!notesopt&&origfunc&&infowords.origin)
    { s = infowords.origin ; 
      a = domcreate('span',null,'style',
                     'color:'+genpic.style.mvisited+';cursor:pointer') ; 
      a.onclick = origfunc ;
    }
    else if(notesopt&&infowords.notes)
    { s = infowords.notes ; 
      a = domcreate('a',null,'href','javascript:quitimg()') ; 
      a.setAttribute('class','m') ; 
    }
    if(s)
    { s = 'Return to ' + s + " [↵ key]" ; 
      a.appendChild(document.createTextNode(s)) ;
      d.appendChild(a) ;
      d.appendChild(domcreate('br')) ;
    }
    div.appendChild(d) ;
    div.appendChild(pixinfodiv(item,title,genpic.style.mfg,genpic.itemparms,
                               genpic.drawparms.img)) ; 
    div.appendChild(helpdiv) ; 
    div.appendChild(pixdocspan()) ;
    return div ; 
  }
}
/* -------------------------------------------------------------------------- */

function genmenu(element,divgen,style)
{ if(!genmenu.menudiv) genmenu.menudiv = domcreate('div') ; 
  if(element=='display') { displaymenu() ; return ; }
  else if(element=='del') { delmenu() ; return ; }
  else if(element=='toggle') 
  { if(genmenu.div) delmenu() ; else displaymenu() ; return ; }
  else if(element=='status') return genmenu.div?1:0 ; 

  genmenu.element = element ; 
  genmenu.divgen = divgen ; 
  genmenu.style = style ; 
  genmenu.div = null ; 

  drawmenuicon(0) ;
  genmenu.menudiv.onclick = displaymenu ; 
  genmenu.element.appendChild(genmenu.menudiv) ; 
  return ; 

  function drawmenuicon(opt) 
  { var i , c = domcreate('canvas') , ctx = c.getContext("2d") ;
    var m = genmenu.menudiv ; 
    while(m.childNodes.length) m.removeChild(m.lastChild) ;
    c.setAttribute('width','28px') ; 
    c.setAttribute('height','28px') ; 

    ctx.fillStyle = genmenu.style.bg ; 
    ctx.fillRect(0,0,28,28) ; 
    ctx.lineWidth = 3 ; 
    ctx.strokeStyle = genmenu.style.fg ;
    if(opt==0) for(i=0;i<3;i++) 
    { ctx.beginPath() ; 
      ctx.moveTo(4,6.5+7*i) ; 
      ctx.lineTo(24,6.5+7*i) ; 
      ctx.stroke() ;
    }
    else
    { ctx.beginPath() ; ctx.moveTo(4,3) ; ctx.lineTo(21,20) ; ctx.stroke() ;
      ctx.beginPath() ; ctx.moveTo(4,20) ; ctx.lineTo(21,3) ; ctx.stroke() ;
    }
    m.appendChild(c) ; 
    m.setAttribute("style","cursor:pointer;width:28px;height:28px;"+
                           "position:absolute;top:0;right:0") ;
  }

  function displaymenu()
  { if(genmenu.div) return ; 
    genmenu.div = genmenu.divgen() ; 
    genmenu.div.setAttribute('style','position:fixed;top:28px;right:4px;' +
                             'color:'+genmenu.style.mfg+';background:' +
                             genmenu.style.mbg+';padding:8px;opacity:0.9;' +
                             'z-index:99') ;
    genmenu.element.appendChild(genmenu.div) ; 
    drawmenuicon(1) ; 
    genmenu.menudiv.onclick = delmenu ; 
  }
  function delmenu()
  { if(!genmenu.div) return ; 
    genmenu.element.removeChild(genmenu.div) ; 
    genmenu.div = null ; 
    drawmenuicon(0) ; 
    genmenu.menudiv.onclick = displaymenu ; 
  }
}
/* -------------------------------------------------------------------------- */

function offerfullscreen(words,col)
{ if(!words||!querycanfullscreen()) return null ; 
  var fullword , fullfunc , s ; 
  if(!col) col = defstyle.mlink ;

  if(queryfullscreen()) 
  { if(words.exit) 
    { fullfunc = function() { exitfullscreen() ; genmenu('del') ; } ; 
      fullword = words.exit ; 
    }
  } 
  else  
  { if(words.enter)
    { fullfunc = function() { enterfullscreen() ; genmenu('del') ; } ; 
      fullword = words.enter ; 
    }
  } 
  if(!fullword) return null ;
  s = domcreate('div',fullword,'style',
                'color:'+col+';cursor:pointer;white-space:nowrap') ;
  s.onclick = fullfunc ; 
  return s ;
}
/* -------------------------------------------------------------------------- */

function getcatval(arr,cat)
{ var val=null,k,field ; 
  if(!arr) return null ;  

  // find an attribute in arr to use when there is no category
  for(k=arr.length-1;!val&&k>=0;k--)
    if(k==0||!arr[k].cat||arr[k].cat.length==0) 
      for(field in arr[k]) if(field!='cat') val = arr[k][field] ;
  if(!cat) return val ; 

  for(k=arr.length-1;k>=0;k--) if(arr[k].cat==cat)
    for(field in arr[k]) if(field!='cat') return arr[k][field] ;
  return val ; 
}
/* -------------------------------------------------------------------------- */

function pixhelpdiv(style)
{ var tr , d=domcreate('div') , t=domcreate('table') ;
  if(!style) style = defstyle ; 
  d.setAttribute('style','color:'+style.mfg+';border-top:solid 1px '+
                 style.mfg+';padding-top:3px;margin-top:3px') ;

  function addcell(tr,style,content,rowspan) 
  { var td = domcreate('td',content,'style',style) ; 
    if(rowspan) td.setAttribute('rowspan',rowspan) ;
    tr.appendChild(td) ;
  }
  // the help table
  var ss='color:'+style.mfg+';font-size:80%' ;
  var sc='color:'+style.mfg+';font-size:80%;text-align:center' ;
  var sr='color:'+style.mfg+';font-size:80%;text-align:right' ;

  t = domcreate('table') ; 
  t.setAttribute('cellspacing',0) ; 
  t.setAttribute('cellpadding',0) ; 
  tr = domcreate('tr') ; 
  addcell(tr,sr,'swipe left') ; addcell(tr,ss,'\u00a0',99) ; 
  addcell(tr,sr,'=') ; addcell(tr,ss,'\u00a0',99) ; 
  addcell(tr,sc,'‘>’ icon') ; addcell(tr,ss,'\u00a0',99) ; 
  addcell(tr,sr,'=') ; addcell(tr,ss,'\u00a0',99) ; 
  addcell(tr,sc,'‘→’ key') ; addcell(tr,ss,'\u00a0',99) ;
  addcell(tr,ss,'= next') ; 
  t.appendChild(tr) ; 

  tr = domcreate('tr') ; 
  addcell(tr,sr,'swipe right') ;  addcell(tr,sr,'=') ;  
  addcell(tr,sc,'‘<’ icon') ; addcell(tr,sr,'=') ;  
  addcell(tr,sc,'‘←’ key') ; addcell(tr,ss,'= prev') ; 
  t.appendChild(tr) ; 

  tr = domcreate('tr') ; 
  addcell(tr,sr,'swipe down') ;  addcell(tr,sr,'=') ;  
  addcell(tr,sc,'‘↵’ icon') ; addcell(tr,sr,'=') ;  
  addcell(tr,sc,'return key') ; addcell(tr,ss,'= back to origin') ; 
  t.appendChild(tr) ; 

  tr = domcreate('tr') ; 
  addcell(tr,sr,' ') ;  addcell(tr,sr,' ') ;  
  addcell(tr,sc,'‘⊕’ icon') ; addcell(tr,sr,'=') ;  
  addcell(tr,sc,'‘↑’ key') ; addcell(tr,ss,'= enlarge') ; 
  t.appendChild(tr) ; 

  tr = domcreate('tr') ; 
  addcell(tr,sr,' ') ;  addcell(tr,sr,' ') ;  
  addcell(tr,sc,'‘⊖’ icon') ; addcell(tr,sr,'=') ;  
  addcell(tr,sc,'‘↓’ key') ; addcell(tr,ss,'= reduce') ; 
  t.appendChild(tr) ; 

  tr = domcreate('tr') ; 
  addcell(tr,sr,' ') ;  addcell(tr,sr,' ') ;  
  addcell(tr,sc,' ') ; addcell(tr,sr,' ') ;  
  addcell(tr,sc,'[shift] ‘↑’') ; addcell(tr,ss,'= higher res') ; 
  t.appendChild(tr) ; 

  tr = domcreate('tr') ; 
  addcell(tr,sr,' ') ;  addcell(tr,sr,' ') ;  
  addcell(tr,sc,' ') ; addcell(tr,sr,' ') ;  
  addcell(tr,sc,'[shift] ‘↓’') ; addcell(tr,ss,'= lower res') ; 
  t.appendChild(tr) ; 

  tr = domcreate('tr') ; 
  addcell(tr,sr,'tap') ;  addcell(tr,sr,'=') ; 
  addcell(tr,sc,'...') ; addcell(tr,sr,'=') ;  
  addcell(tr,sc,'spacebar') ; addcell(tr,ss,'= start/stop video') ; 
  t.appendChild(tr) ; 

  tr = domcreate('tr') ; 
  addcell(tr,sr,' ') ;  addcell(tr,sr,' ') ; 
  addcell(tr,sr,' ') ; addcell(tr,sr,' ') ;  
  addcell(tr,sc,'‘f’ key') ; addcell(tr,ss,'= enter full screen') ; 
  t.appendChild(tr) ; 

  tr = domcreate('tr') ; 
  addcell(tr,sr,' ') ;  addcell(tr,sr,' ') ; 
  addcell(tr,sr,' ') ; addcell(tr,sr,' ') ;  
  addcell(tr,sc,'‘m’ key') ; addcell(tr,ss,'= menu/info') ; 
  t.appendChild(tr) ; 

  d.appendChild(t) ;
  return d ;
}
function pixdocspan(style)
{ var a=domcreate('a',null,'class','m') , span=domcreate('div') ;
  if(!style) style = defstyle ; 
  a.setAttribute('href','http://www.masterlyinactivity.com/software/pix.html') ;
  a.setAttribute('target','_blank') ; 
  a.appendChild(document.createTextNode('pix.js documentation')) ;

  span.appendChild(a) ; 
  span.appendChild(document.createTextNode(' ')) ;
  span.appendChild(newtabdiv(style.mfg,style.mbg)) ;
  span.setAttribute('style','border-top:solid 1px '+
                            style.mfg+';padding-top:3px;margin-top:3px') ; 
  return span ; 
}
/* -------------------------------------------------------------------------- */

function pixinfodiv(item,sectname,col,itemparms,img)
{ var d=domcreate('div'),ind,r,k,s,i,sizes=item.sizes,ilist ;
  var sfx,res=null,sizeno=itemparms.sizeno,h=item.hithumb,res=itemparms.res ; 
  if(!col) col = defstyle.mfg ; 
  d.setAttribute('style','color:'+col) ; 

  function shapelist(ilist,shape)
  { var s,k ; 
    ilist.sort(function(a,b) { return a[1]-b[1] ; }) ;
    // print the shapes
    for(s='',k=0;k<ilist.length;k++) 
    { if(k>0) { if(k==ilist.length-1) s += ' and ' ; else s += ', ' ; }
      s += wxh(shape,ilist[k][1]) ; 
    }
    return s ; 
  }
  function wxh(shape,q)
  { return (shape[0]*q).toFixed(0) + '×' + (shape[1]*q).toFixed(0) ; }

  s = 'Name: ' + item.name ;
  if(item.serialno>0) s += '('+item.serialno+')' ;
  if(item.visibility=='*') s += ' ★' ;
  d.appendChild(document.createTextNode(s)) ;
  d.appendChild(domcreate('br')) ;

  d.appendChild(document.createTextNode('Title: '+item.title)) ;
  d.appendChild(domcreate('br')) ;

  if(sectname) 
  { d.appendChild(document.createTextNode('Section: '+sectname)) ;
    d.appendChild(domcreate('br')) ;
  }

  if(item.cat)
  { d.appendChild(document.createTextNode('Category: '+item.cat)) ;
    d.appendChild(domcreate('br')) ;
  }

  var ddash=domcreate('div',null,'style','font-size:90%;'+
         'border-top:solid 1px '+col+';padding-top:3px;margin-top:3px;') ; 

  // how many shapes?
  if(item.extn=='.mp4') s = 'Size: ' + wxh(item.shape,1) ; 
  else
  { q = sizes[0].scale ; 
    for(ilist=[],ind=0;ind<sizes.length;ind++) if(!sizes[ind].type) 
      ilist.push([ind,sizes[ind].scale]) ;
    s = 'Available in ' + ilist.length + ' size' + (ilist.length>1?'s: ':': ') ;
    s += shapelist(ilist,[item.shape[0]/q,item.shape[1]/q]) ; 
  }
  ddash.appendChild(document.createTextNode(s)) ;
  ddash.appendChild(domcreate('br')) ;

  // print the thumb shape
  s = 'Thumb: ' + wxh(item.thumbshape,1) ;
  if(h) 
  { for(ilist=[],ind=0;ind<h.length;ind++) 
      ilist.push([ind,h[ind].scale]) ; 
    s +=' (hi-res: ' + shapelist(ilist,item.thumbshape) + ')' ;
  }
  ddash.appendChild(document.createTextNode(s)) ;
  ddash.appendChild(domcreate('br')) ;

  // print the raw shape
  if(item.rawshape) for(ind=0;ind<sizes.length;ind++) if(sizes[ind].type=='raw')
  { s = 'Raw: ' + wxh(imgshape(item,ind),1) ;
    ddash.appendChild(document.createTextNode(s)) ;
    ddash.appendChild(domcreate('br')) ;
  }

  if(sizeno<0) s = 'thumbnail' ;
  else if(sizes[sizeno].type=='raw') s = 'raw' ;
  else
  { if(item.extn=='.mp4') q = 1 ; 
    else q = sizes[sizeno].scale / sizes[0].scale ;
    s = wxh(item.shape,q) ;
  }
  if(res&&(res>1.01||res<0.99)) s += ' (res=' + pixscale(res) + 'x)' ; 
  ddash.appendChild(document.createTextNode('Currently displaying: '+s)) ;
  ddash.appendChild(domcreate('br')) ;
  d.appendChild(ddash) ;
  return d ; 
}
/* -------------------------------------------------------------------------- */

function newtabdiv(fg,bg)
{ var div = domcreate('div') ; 
  var svgns = "http://www.w3.org/2000/svg" ;  
  var lsvg = document.createElementNS(svgns,"svg") , elt;
  if(!fg) fg = 'black' ; 
  if(!bg) bg = 'white' ; 

  div.setAttribute('style','width:12px;height:12px;display:inline-block') ; 
  lsvg.setAttributeNS(null,'height',12) ; 
  lsvg.setAttributeNS(null,'width',12) ; 

  function genrect(x,y,w,h,s,rx)
  { var elt = document.createElementNS(svgns,"rect") ;
    elt.setAttributeNS(null,'x',x) ; 
    elt.setAttributeNS(null,'y',y) ; 
    elt.setAttributeNS(null,'width',w) ; 
    elt.setAttributeNS(null,'height',h) ; 
    elt.setAttributeNS(null,'style',s) ; 
    if(rx) 
    { elt.setAttributeNS(null,'rx',rx) ; elt.setAttributeNS(null,'ry',rx) ; }
    return elt ;
  }
  function genpoly(p)
  { var elt = document.createElementNS(svgns,"polygon") ;
    elt.setAttributeNS(null,'points',p) ; 
    elt.setAttributeNS(null,'style','fill:'+fg) ; 
    return elt ;
  }

  lsvg.appendChild(genrect('0.5','2.5',9,9,
                           "fill:"+bg+";stroke-width:1;stroke:"+fg,'1.5')) ; 
  lsvg.appendChild(genrect(6,0,6,6,"fill:"+bg)) ; 
  lsvg.appendChild(genpoly("6,2 6,3 7,3")) ; 
  lsvg.appendChild(genpoly("9,6 10,6 10,5")) ; 
  lsvg.appendChild(genpoly("7,0 12,0 12,5")) ; 
  lsvg.appendChild(genpoly("5.65,5.65 6.35,6.35 12,0.7 11.3,0")) ; 

  div.appendChild(lsvg) ; 
  return div ; 
}
/* ---------------------------------- swipes -------------------------------- */

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==null)
  { 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==null)
  { 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,action=null ;
  if(fingersep!=null&&startsep!=null&&here.ncol==null) ;
/* let the operating system handle pinches
  { e.preventDefault() ; 
    if(fingersep>startsep+50) action = 38 ; 
    else if(startsep>fingersep+50) action = 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&&swipetime>20)
    { e.preventDefault() ; 
      if(Math.abs(xloc-xstart)>100&&Math.abs(yloc-ystart)<100)
      { if(xloc>xstart) action = 37 ; else action = 39 ; }
      else if(yloc>ystart+100&&Math.abs(xloc-xstart)<100) action = 13 ; 
    }
    else if(v<25&&swipetime<500) { e.preventDefault() ; action = 32 ; }
  }
  xloc = yloc = xstart = ystart = fingersep = startsep = swipetime = null ; 
  if(action) simulate(action)
}
/* -------------------------------------------------------------------------- */

function selcat(pl,q)
{ var i,j,jj,k,r=[],gpsno,carry,rind,keepgps,ogps,ndrop,nkeep,cat,rr,item ; 
  var showhidden=null,ll,starl=[],nsort,sno,ind,maxthumb,end ;
  var sect=pl.rawsect ; 
  var disp = new Array(sect.length)  ;
  if(q) cat = q.cat ; else cat = null ; 
  pl.vis = { star:0 , none:0 , def:0 , hidden:0 } ;

  if(!q||q.mode.indexOf('v')>=0||q.mode.indexOf('a')>=0) showhidden = 'a' ; 
  else if(q.mode.indexOf('*')>=0) showhidden = '*' ;
  else if(q.mode.indexOf('h')>=0) showhidden = 'h' ;

  function keepquery(item)
  { var v , failval = 0 ;
    if(q&&q.name&&q.name==item.name) failval = 1 ; 
    // test for category
    if(cat&&((!item.cat)||item.cat!=cat)) return failval ; 
    // update pl.vis (which is whether this cat has items with a given display)
    v = item.visibility ;
    if(v=='*') pl.vis.star = 1 ; 
    else if(v=='none') pl.vis.none = 1 ; 
    else pl.vis.def = 1 ; 
    // test for visibility
    if(showhidden=='a'||failval) return 1 ; 
    if((showhidden=='*'&&v!='*')||(showhidden=='h'&&v!='none')) return 0 ; 
    else if(showhidden!='h'&&v=='none') return 0 ; 
    else return 1 ;
  }

  for(r=[],i=0;i<sect.length;i++)
  { ll = sect[i].list ; 
    for(nkeep=ndrop=carry=gpsno=0,j=0;j<ll.length;j++) 
    { if(ll[j].display=='|') gpsno += 1 ; 
      if(keepquery(ll[j])) 
      { disp[j] = { barred:((ll[j].display=='|')||carry) , gpsno:gpsno } ; 
        carry = 0 ; 
        nkeep += 1 ; 
      }
      else 
      { disp[j] = { barred:0 , gpsno:null } ; 
        carry = carry || (ll[j].display=='|') ; 
        ndrop += 1 ; 
      }
    }

    // section title - may need to compact the gps tracks
    if(nkeep>0&&showhidden!='*'&&showhidden!='h')
    { r.push(clone(sect[i])) ; 
      if(!sect[i].gps||gpsno+1!=sect[i].gps.length) r[r.length-1].nomatch = 1 ; 
      // deal with the tricky case in which the gps tracks correspond to the
      // vertical bars, and we need to see if any need to be discarded
      else for(ogps=null,r[r.length-1].gps=[],j=0;j<ll.length;j++) 
        if(disp[j].gpsno!=null&&disp[j].gpsno!=ogps)
      { ogps = disp[j].gpsno ; r[r.length-1].gps.push(sect[i].gps[ogps]) ; }
    }

    // now add the images
    for(rr=[],j=0;j<ll.length;j++) if(disp[j].gpsno!=null) 
    { rr.push(ll[j]) ; // for safari
      rr[rr.length-1].barred = 
        (disp[j].barred&&showhidden!='*'&&showhidden!='h')?'|':null ;  
    }
    if(showhidden=='*'||showhidden=='h') starl = starl.concat(rr) ; 
    else if(nkeep>0) r[r.length-1].list = rr ; 
  }

  if(showhidden=='*'||showhidden=='h') r = [ { list:starl } ] ; 

  // sort photos by name to check for dupes 
  for(nsort=sno=0;sno<r.length;sno++) nsort += r[sno].list.length ; 
  sortlist = new Array(nsort) ;
  for(nsort=sno=0;sno<r.length;sno++)
    for(ll=r[sno].list,i=0;i<ll.length;i++) 
      sortlist[nsort++] = { name:ll[i].name , sno:sno , ind:i } ;

  sortlist.sort( function(a,b)
                 { var ret = a.name.localeCompare(b.name) ; 
                   if(ret!=0) return ret ; 
                   else if(a.sno!=b.sno) return a.sno-b.sno ;
                   else return a.ind - b.ind ;
                 } ) ;

  // 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++) 
      r[sortlist[end].sno].list[sortlist[end].ind].serialno = end - ind ; 

  // now find the max thumb dimensions
  for(pl.maxthumb=0,sno=0;sno<r.length;sno++) if(!r[sno].layout)
    for(ll=r[sno].list,i=0;i<ll.length;i++)
      if(ll[i].thumbshape[0]>pl.maxthumb) pl.maxthumb = ll[i].thumbshape[0] ; 

  pl.sect = r ; 
}
/* -------------------------------------------------------------------------- */

function mp4icon()
{ var c , ctx , ratio = window.devicePixelRatio||1 , sqrt3 = Math.sqrt(3) ;
  var d = domcreate('div',null,'style','position:absolute;bottom:6px;' + 
                                       'left:6px;width:16px;height:16px') ; 
  c = domcreate('canvas',null,'style','width:16px;height:16px') ; 
  c.setAttribute('width',16*ratio) ; 
  c.setAttribute('height',16*ratio) ; 
  ctx = c.getContext("2d") ;
  ctx.scale(ratio,ratio) ; 

  ctx.fillStyle = "silver" ; 
  ctx.beginPath() ;
  ctx.arc(8,8,8,0,2*Math.PI,false) ; 
  ctx.fill() ; 

  ctx.fillStyle = "black" ; 
  ctx.beginPath() ;
  ctx.moveTo(4,8+4*sqrt3) ; 
  ctx.lineTo(16,8) ; 
  ctx.lineTo(4,8-4*sqrt3) ; 
  ctx.lineTo(4,8+4*sqrt3) ; 
  ctx.fill() ; 

  d.appendChild(c) ; 
  return d ;
}
/* -------------------------------------------------------------------------- */