function TomsThumbnailer(preload) {

    var PREFERREDTHUMBNAILPOS = {
        HORIZONTAL:  1, // >=0: right;  <0: left
        VERTICAL:  1    // >=0: bottom; <0: top
    }
    var PREFERREDSIZETAGPOS = {
        HORIZONTAL: -1, // >=0: right;  <0: left
        VERTICAL:  1    // >=0: bottom; <0: top
    }


    var CONST = {
        CHECKERS: 'url(data:image/gif;base64,R0lGODlhBAAEAKECAMzMzP///wAAAAAAACH5BAEKAAIALAAAAAAEAAQAAAIGTACGqBkFADs=)',
        ERRORCURSOR: 'url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAAXNSR0IArs4c6QAAAoFJREFUOMt'+
                    '11M9rXGUUxvHPvXduJjdTY/NDYlsbRyeNiQTFARW7Coi0ULFamuDKTdXSbf0LFFy0IKhY1IqgUVDQRReCIIiiWF1IG03BWKmmFklj'+
                    'lcmkdNokk1wXvZNca3w27+K875dzzvPwRv6tEDtwM2o2VhEjaOLqRheCgNGYz4tMBFQycF5xxFiRLyLeQPd/ILgn4fRDrDxMo8SxD'+
                    'LYGCXmkzKk9NO9kMeRoHhYGjCacfoz0BOlnpPto9PBeyN0oxoyVOfUi6dekr5IOsFjgOPoiDIQcq1J9jmAbSugnblCepXeZngqHxq'+
                    'nuIuzAVnQR/cDwFeICVmJ+2cS9MUmAVZTxJJ2LjE+y+3F69hAVkaKANmzi0hx/RKit8uMcyTwj/cRdGazruj2F+yntJGzPdtHESbz'+
                    'J2fMcWeHdKKvVm0z/SV+D8m0UW7AEvVkHwTpk9X3OT3NkmY9wuQVKUVvk51l6a9w1QiG5wVb4DhNcOMMrS7yDy27MySrn6nw7ycJs'+
                    '1lGaQVIsY4r0V2aW+BJXWm+jfE5i9lY4tJ+BnYSFHCTIxruFoEHXRTZfYwp1pFEubKPbeX6c6qNE7dnjGCvZ2TJgO8UFBn8naTKNW'+
                    'pTFfm8/LzzDfbty7qT4Hp+QbiPozMGGiBcYuUjfVX6KkLQz9iBPPEWUZIAUX5G+zYVvmKzRezttPRms87qj8Rk6/uJklO3wtwLDZX'+
                    'ZsQUfmzuvMTXH0Gq/NcGudoQeIbsLf+JT6WV6e5+O1TZfoq3LiJZaOk97BuYBn0Zq0HPDWfhofkh5kfisHSuv1dXUzOMQHFWbaeDp'+
                    'gc/6vCqh0MzHMTJnDG0Jy2oJ9/v/SIHZnaVjTP6JKtGDAViSYAAAAAElFTkSuQmCC) 9 9, auto'
    }

    var globalPreload = !!preload;
    var quirksMode = document.compatMode == 'BackCompat';

    var setInnerHTML = function(el, string) {
        var self = arguments.callee;
        if (typeof self.supportsInnerHTML == 'undefined') {
            var testParent = document.createElement('div');
            testParent.innerHTML = '<br/>';
            self.supportsInnerHTML = (testParent.firstChild.nodeType == Node.ELEMENT_NODE);
        }
        if (self.supportsInnerHTML) {
            el.innerHTML = string;
        } else {
            if (!self.cleanDocumentObject) {
                /* this is where we get a 'clean' document object */
                var f = document.createElement('iframe');
                f.style.setProperty('display', 'none', 'important');
                f.src = 'data:text/html,<!DOCTYPE html><html><title></title></html>';
                document.body.appendChild(f);
                self.cleanDocumentObject = f.contentDocument;
                document.body.removeChild(f);
            }

            /* let Browser do the parsing */
            var div = self.cleanDocumentObject.createElement('div');
            div.innerHTML = string; // this does work

            /* move childNodes */
            while(el.firstChild) {
                el.removeChild(el.firstChild);
            }
            while(div.firstChild) {
                el.appendChild(document.adoptNode(div.firstChild));
            }
            delete div;
        }
    }

    var stringToDOM = function(inp, firstChildOnly) {
        var newParent = document.createElement('div');
        setInnerHTML(newParent, inp);
        if (firstChildOnly)
            return newParent.firstChild;
        else
            return newParent.childNodes;
    }

    function addEventWatcher(el, evt, fun, cap) {
        if (typeof window.addEventListener == 'function') {
            return el.addEventListener(evt, fun, cap);
        } else {
            el.attachEvent('on' + evt, function() { fun(window.event) } );
        }
    }

    var cacheSizes = function() {
        var scrollbarTest = quirksMode ? document.body : document.documentElement;
        return {
            windowInnerWidth : window.innerWidth,
            windowInnerHeight : window.innerHeight,
            bodyHasVerticalScrollbar: (scrollbarTest.scrollHeight != scrollbarTest.clientHeight),
            bodyHasHorizontalScrollbar: (scrollbarTest.scrollWidth != scrollbarTest.clientWidth)
        };
    }
    var cachedSizes = cacheSizes();
    addEventWatcher(window, 'resize', function() {cachedSizes = cacheSizes()}, false);
    // faster than repeatedly accessing window.innerWidth etc.

    var moveToPos = function(el, cursorX, cursorY, alignH, alignV) {
        if (!el) return;
        var SPACING = { TOP: 10, BOTTOM: cachedSizes.bodyHasHorizontalScrollbar ? 30 : 10,
                        LEFT: 10, RIGHT: cachedSizes.bodyHasVerticalScrollbar ? 30 : 10 };
        var CURSORMARGIN = { TOP: 10, BOTTOM: 25, LEFT: 15, RIGHT: 20 };
        var iW = cachedSizes.windowInnerWidth;
        var iH = cachedSizes.windowInnerHeight;
        var elWidth = el.cachedWidth;
        if (!elWidth) elWidth = el.cachedWidth = el.offsetWidth || el.naturalWidth || el.width || parseInt(el.style.width) || 0;
        var elHeight = el.cachedHeight;
        if (!elHeight) elHeight = el.cachedHeight = el.offsetHeight || el.naturalHeight || el.height || parseInt(el.style.height) || 0;
        var minX = SPACING.LEFT;
        var maxX = iW - SPACING.RIGHT;
        if (maxX <= minX) { minX = 0; maxX = iW; }
        var minY = SPACING.TOP;
        var maxY = iH - SPACING.BOTTOM;
        if (maxY <= minY) { minY = 0; maxY = iH; }
        if (cursorX < minX) cursorX = minX;
        if (cursorX > maxX) cursorX = maxX;
        if (cursorY < minY) cursorY = minY;
        if (cursorY > maxY) cursorY = maxY;
        var rightX = cursorX + CURSORMARGIN.RIGHT;
        var leftX = cursorX - CURSORMARGIN.LEFT - elWidth;
        var midX = (minX + maxX - elWidth) / 2;
        var bottomY = cursorY + CURSORMARGIN.BOTTOM;
        var topY = cursorY - CURSORMARGIN.TOP - elHeight;
        var midY = (minY + maxY - elHeight) / 2;
        var newX = midX;
        var newY = midY;
        if (alignH < 0) {
            if (leftX >= minX)
                newX = leftX;
            else if (rightX + elWidth <= maxX)
                newX = rightX;
        } else {
            if (rightX + elWidth <= maxX)
                newX = rightX;
            else if (leftX >= minX)
                newX = leftX;
        }
        if (alignV < 0) {
            if (topY >= minY)
                newY = topY;
            else if (bottomY + elHeight <= maxY)
                newY = bottomY;
        } else {
            if (bottomY + elHeight <= maxY)
                newY = bottomY;
            else if (topY >= minY)
                newY = topY;
        }
        if (newX == midX && newY == midY) { // push away, leave biggest visible area
            var areaTop = Math.min(elWidth, iW) * (topY + elHeight);
            var areaBottom = Math.min(elWidth, iW) * (iH - bottomY);
            var areaLeft = (leftX + elWidth) * Math.min(elHeight, iH);
            var areaRight = (iW - rightX) * Math.min(elHeight, iH);
            switch(Math.max(areaTop, areaBottom, areaLeft, areaRight)) {
                case areaTop:
                    newY = topY;
                    break;
                case areaBottom:
                    newY = bottomY;
                    break;
                case areaLeft:
                    newX = leftX;
                    break;
                default:
                    newX = rightX;
                    break;
            }
        }
        el.style.left = newX + 'px';
        el.style.top = newY + 'px';
    }

    var Thumbnail = function(domStringOrDOM, url, showSizeTags, errorHandler) {
        this.mainContent = {};
        this.size = {w:0, h:0};
        if (typeof domStringOrDOM == 'string') {
            this.mainContent = stringToDOM(domStringOrDOM.replace(/src="/, 'id="').replace(/%checkers%/g, CONST.CHECKERS), true);
        } else if (!domStringOrDOM) {
            if (!url) return -1;
            this.mainContent = document.createElement('img');
            this.mainContent.id = url;
            this.mainContent.style.border = '1px solid gray';
        } else {
            this.mainContent = domStringOrDOM;
        }
        this.sizeTag =  document.createElement('label');
        this.sizeTag.style.visibility = 'hidden';
        var imgs = (this.mainContent.tagName.toLowerCase() == 'img') ? [this.mainContent] : this.mainContent.getElementsByTagName('img');
        if (imgs.length > 1) throw new Error('Thumbnail must not have more that one img element');
        var img = imgs[0];
        if (img) {
            img.thumbnail = this;
            this.mainContent.style.visibility = 'hidden';
            this.isLoading = true;
            this.failedLoading = false;
            addEventWatcher(img, 'load', function() {
                var thn = this.thumbnail;
                thn.size = {w: this.naturalWidth || this.width, h: this.naturalHeight || this.height}
                if (showSizeTags) {
                    with (thn.sizeTag.style) {
                        color = 'black';
                        backgroundColor = 'lightyellow';
                        position = 'fixed';
                        border = '1px solid black';
                        padding = '.2em .5em';
                        fontFamily = 'monospace';
                        border = '1px inset gray';
                        zIndex = '32010';
                        MozBorderRadius = '3px';
                    }
                    thn.sizeTag.appendChild(document.createTextNode(thn.size.w + ' \u00D7 ' + thn.size.h));
                    thn.sizeTag.cachedWidth = thn.sizeTag.cachedHeight = undefined;
                }
                if(thn.mainContent.innerHTML)
                    setInnerHTML(thn.mainContent, thn.mainContent.innerHTML.replace(/%width%/g, thn.size.w).replace(/%height%/g, thn.size.h));
                thn.isLoading = false;
                if (thn.showAfterLoading) thn.showAt(thn.currentCoords.x, thn.currentCoords.y);
            }, false);
            if (errorHandler) {
                addEventWatcher(img, 'error', function(e) {
                    this.failedLoading = true;
                    this.isLoading = false;
                    errorHandler(this, e);
                }, false);
            }
            img.src = img.id.replace(/%href%/g, url);
            img.id = '';
        }
        this.mainContent.className = 'TomsThumbnailer';
        this.mainContent.style.position = 'fixed';
        this.mainContent.style.zIndex = '32000';

        this.addToDOM = function() {
            if (!this.mainContent.parentNode)
                document.body.appendChild(this.mainContent);
            if (this.sizeTag && !this.sizeTag.parentNode)
                document.body.appendChild(this.sizeTag);
        }
        this.removeFromDOM = function() {
            if (this.mainContent.parentNode)
                this.mainContent.parentNode.removeChild(this.mainContent);
            if (this.sizeTag && this.sizeTag.parentNode)
                this.sizeTag.parentNode.removeChild(this.sizeTag);
        }
        this.showAt = function(x, y) {
            if (this.failedLoading)
                return;
            if (this.isLoading) {
                this.showAfterLoading = true;
                this.currentCoords = {x: x, y: y};
                return;
            }
            this.hide();
            this.addToDOM();
            this.moveTo(x,y);
            this.show();
        }
        this.moveTo = function(x,y) {
            if (this.failedLoading)
                return;
            if (this.isLoading) {
                this.currentCoords = {x: x, y: y};
                return;
            }
            moveToPos(this.mainContent, x, y, PREFERREDTHUMBNAILPOS.HORIZONTAL, PREFERREDTHUMBNAILPOS.VERTICAL);
            moveToPos(this.sizeTag, x, y, PREFERREDSIZETAGPOS.HORIZONTAL, PREFERREDSIZETAGPOS.VERTICAL);
        }
        this.show = function() {
            if (this.failedLoading)
                return;
            if (this.isLoading) {
                this.showAfterLoading = true;
                return;
            }
            this.mainContent.style.visibility = '';
            if (this.sizeTag)
                this.sizeTag.style.visibility = '';
        }
        this.hide = function() {
            if (this.isLoading) {
                this.showAfterLoading = false;
                return;
            }
            this.removeFromDOM();
            this.mainContent.style.visibility = 'hidden';
            if (this.sizeTag)
                this.sizeTag.style.visibility = 'hidden';
        }
    }

    this.addThumbnail = function(el, preloadOne, showSizeTags, domStringOrDOM) {
        if (typeof el == 'string')
            el = document.getElementById(el);
        var preload = (/^(undefined|null)$/.test(typeof preloadOne)) ? preloadOne : globalPreload;
        var errorHandler = function() { el.style.cursor = CONST.ERRORCURSOR; el.style.textDecoration = 'line-through'; }
        if (preload)
            el.thumbnail = new Thumbnail(domStringOrDOM, el.href, showSizeTags, errorHandler);

        addEventWatcher(el, 'mouseover', function(e) {
            if (this.removeAttribute) this.removeAttribute('title'); else this.title = '';
            if (!this.thumbnail)
                this.thumbnail = new Thumbnail(domStringOrDOM, el.href, showSizeTags, errorHandler);
            this.thumbnail.showAt(e.clientX, e.clientY);
        }, false);

        addEventWatcher(el, 'mousemove', function(e) {
            if (this.thumbnail) {
                this.thumbnail.moveTo(e.clientX, e.clientY);
            }
        }, false);

        addEventWatcher(el, 'click', function() {
            if (this.thumbnail)
                this.thumbnail.hide();
        }, false);

        addEventWatcher(el, 'mouseout', function() {
            if (this.thumbnail)
                this.thumbnail.hide();
        }, false);
    }

    this.addThumbnailsToAllLinks = function(el, preload, showSizeTags, DOMString) {
        el = el || document;
        var links = el.getElementsByTagName('a');
        if (DOMString)
            var domTemplate = stringToDOM(DOMString.replace(/src="/, 'id="').replace(/%checkers%/g, CONST.CHECKERS), true);
        for (var i = 0; i < links.length; i++) {
            this.addThumbnail(links[i], preload, showSizeTags, (DOMString ? domTemplate.cloneNode(true) : null));
        }
    }

    return this;
}