Large

Mouseover Popup Image Viewer

By kuehlschrank Last update May 13, 2013 — Installed 92,547 times.

There are 73 previous versions of this script.

Add Syntax Highlighting (this will take a few seconds, probably freezing your browser while it works)

// ==UserScript==
// @id             org.userscripts.users.kuehlschrank.MouseoverPopupImageViewer
// @name           Mouseover Popup Image Viewer
// @description    Shows larger version of thumbnails.
// @version        2013.5.12
// @author         kuehlschrank
// @homepage       http://userscripts.org/scripts/show/109262
// @icon           https://s3.amazonaws.com/uso_ss/icon/109262/large.png
// @updateURL      https://userscripts.org/scripts/source/109262.meta.js
// @downloadURL    https://userscripts.org/scripts/source/109262.user.js
// @include        http*
// ==/UserScript==

'use strict';

var d = document, wn = window, $ = function(id) { return d.getElementById(id); }, cur = {}, hosts;

var cfg = {
	delay: GM_getValue('delay', 500),
	thumbsonly: GM_getValue('thumbsonly', true),
	key: GM_getValue('key', false),
	zoom: GM_getValue('zoom', 'context'),
	img: GM_getValue('img', false),
	css: GM_getValue('css', ''),
	hosts: GM_getValue('hosts', '')
};

function loadHosts() {
	var hosts = [
		/* DO NOT EDIT THE CODE. USE GREASEMONKEY ICON -> USER SCRIPT COMMANDS -> SET UP... */
		{r:/500px\.com\/photo\//, q:'#mainphoto'},
		{r:/abload\.de\/image/, q:'#image'},
		{r:/(ecx\.images-amazon\.com\/images\/I\/.+?)\./, s:function(m, node) { return node.parentNode.classList.contains('main-image-inner-wrapper') || node.parentNode.querySelector('#twister-main-image') ? '' : 'http://' + m[1] + '.jpg'; }},
		{r:/depic\.me\//, q:'#pic'},
		{r:/deviantart\.com\/art\//, q:['#gmi-ResViewSizer_img', 'img.smshadow']},
		{r:/fastpic\.ru\/view\//, q:'#image'},
		{r:/(fbcdn|fbexternal).*?(app_full_proxy|safe_image).+?(src|url)=(http.+?)[&\"']/, s:function(m, node) { return decodeURIComponent(m[4]); }, html:true},
		{r:/(https?:\/\/(fbcdn-[\w\.\-]+akamaihd|[\w\.\-]+?fbcdn)\.net\/[\w\/\.\-]+?)_[a-z]\.jpg/, s:function(m, node) { if(node.parentNode.outerHTML.indexOf('hovercard') > -1) return ''; var gp = node.parentNode.parentNode; if(node.outerHTML.indexOf('profile') > 1 && gp.href && gp.href.indexOf('/photo') > -1) return false; return m[1].replace(/\/[spc][\d\.x]+/g, '') + '_n.jpg'; }},
		{r:/facebook\.com\/photo/, q:['#fbPhotoImage', '#root img', '#root i.img']},
		{r:/firepic\.org\/\?v=/, q:'.view img[src*="firepic.org"]'},
		{r:/flickr\.com\/photos\/([0-9]+@N[0-9]+|[a-z0-9_\-]+)\/([0-9]+)/, s:'http://www.flickr.com/photos/$1/$2/sizes/l/', q:'#allsizes-photo > img'},
		{r:/hotimg\.com\/image\/([a-z0-9]+)/i, s:'http://www.hotimg.com/direct/$1'},
		{r:/imagearn\.com\/image/, q:'#img', xhr:true},
		{r:/imagefap\.com\/(image|photo)/, q:'#gallery + noscript'},
		{r:/imagebam\.com\/image\//, q:'img[id]'},
		{r:/(imageban\.(ru|net)|imagerise\.com|imgnova\.com|cweb-pix\.com|sluhost\.com(\/1)?)\/show/, q:'#img_obj', xhr:true},
		{r:/imagebunk\.com\/image/, q:'#img_obj', xhr:true},
		{r:/imagehaven\.net\/img\.php/, q:'#image'},
		{r:/imageshack\.us\/((i|f|photo)\/|my\.php)/, q:['div.codes > div + div', '#main_image, #fullimg']},
		{r:/imageshost\.ru\/photo\//i, q:'#bphoto'},
		{r:/image(team|nice)\.org\/img/, q:'img[alt="image"]'},
		{r:/(imagetwist|imageshimage)\.com\/[a-z0-9]+\/?(.+\.html)?/, q:'img.pic', xhr:true},
		{r:/imageupper\.com\/i\//, q:'#img', xhr:true},
		{r:/imagepix\.org\/image\/(.+)\.html$/, s:'http://imagepix.org/full/$1.jpg', xhr:true},
		{r:/(img[0-9]+\.imageporter\.com\/i\/[0-9]+\/[a-z0-9]+)_t\.jpg/i, s:'http://$1.jpg', xhr:true},
		{r:/imagevenue\.com\/img\.php/, q:'#thepic'},
		{r:/imagewaste\.com\/pictures\/(.+)/, s:'http://www.imagewaste.com/pictures/big/$1', xhr:true},
		{r:/imagezilla\.net\/show\//, q:'#photo', xhr:true},
		{r:/media-imdb\.com\/images\/.+?\.jpg/, s:function(m, node) { return node.src.replace(/V1\.?_.+?\./g, ''); }},
		{r:/imgbox\.com\/([a-z0-9]+)$/i, q:'#img', xhr:location.hostname != 'imgbox.com'},
		{r:/imgchili\.com\/show/, q:'#show_image', xhr:true},
		{r:/imgmoney\.com\/img-/, q:'img.centred_resized', xhr:true, post:'imgContinue=Continue%20to%20image%20...%20'},
		{r:/(imgrill\.com\/upload\/)small(\/.+?\.jpg)/, s:'http://$1big$2', xhr:true},
		{r:/imgtheif\.com\/image\//, q:'a > img[src*="/pictures/"]'},
		{r:/imgur\.com\/a\/([a-z0-9]+)/i, s:'http://imgur.com/a/$1/noscript', g:{entry:'div.image', image:'img', caption:'h2'}},
		{r:/imgur\.com\/(gallery\/|r\/[a-z]+\/|[a-z0-9]+#)?([a-z0-9]{5,}[^\.,]*$)/i, s:'http://i.imgur.com/$2.jpg'},
		{r:/instagr(\.am|am\.com)\/p\/([a-z0-9_\-]+)/i, s:'http://instagr.am/p/$2/media/?size=l'},
		{r:/itmages\.ru\/image\/view\//, q:'#image'},
		{r:/gifbin\.com\/.+\.gif/, xhr:true},
		{r:/googleusercontent\.com\/gadgets\/proxy.+?(http.+?)&/, s:function(m, node) { return decodeURIComponent(m[1]); }},
		{r:/[a-z0-9]+\.googleusercontent\.com\/.+/i, s:function(m, node) { if(node.outerHTML.match(/favicons\?|\b(Ol Rf Ep|Ol Zb ag|Zb HPb|Zb Gtb|Rf Pg)\b/)) return ''; return 'https://' + m[0].replace(/\/(s\d{2,}[ck\-]*?|w\d+-h\d+(-p)?)\//g, '/s0/'); }},
		{r:/heberger-image\.fr\/images/, q:'#myimg'},
		{r:/hostingkartinok\.com\/show-image\.php.*/, q:'.image img'},
		{r:/kinopoisk\.ru\/picture\/.*/, q:'#image'},
		{r:/(lazygirls\.info\/.+_.+?\/[a-z0-9_]+)($|\?)/i, s:'http://www.$1?celebrity=a', q:'img.photo', xhr:location.hostname != 'www.lazygirls.info'},
		{r:/ld-host\.de\/show/, q:'#image'},
		{r:/listal\.com\/(view)?image\/([0-9]+)/, s:'http://www.listal.com/image/$2/0full.jpg'},
		{r:/lostpic\.net\/\?(photo|view)/, q:'.casem img'},
		{r:/memegenerator\.net\/instance\/([0-9]+)/, s:'http://images.memegenerator.net/instances/500x/$1.jpg'},
		{r:/(photos\.modelmayhem\.com\/photos\/[0-9a-z\/]+)_m\.jpg/, s:'http://$1.jpg'},
		{r:/(photos\.modelmayhem\.com\/avatars\/[0-9a-z\/]+)_t\.jpg/, s:'http://$1_m.jpg'},
		{r:/(min\.us|minus\.com)\/l([a-z0-9]+)$/i, s:'http://i.min.us/i$2.jpg'},
		{r:/(panoramio\.com\/.*?photo(\/|_id=)|google\.com\/mw-panoramio\/photos\/[a-z]+\/)(\d+)/, s:'http://static.panoramio.com/photos/original/$3.jpg'},
		{r:/(\d+\.photobucket\.com\/.+\/)(\?[a-z=&]+=)?(.+\.(jpe?g|png|gif))/, s:'http://i$1$3', xhr:location.hostname.indexOf('photobucket.com') == -1},
		{r:/(photosex\.biz|posteram\.ru)\/.+?id=/i, q:'img[src*="/pic_b/"]', xhr:true},
		{r:/pic4all\.eu\/(images\/|view\.php\?filename=)(.+)/, s:'http://$1/images/$3'},
		{r:/piccy\.info\/view3\/(.*)\//, s:'http://piccy.info/view3/$1/orig/', q:'#mainim'},
		{r:/(badimg\.com|image\.imagepremium\.com|miragepics\.com|myadultimage\.com|picszone\.net|r70\.info|rupix\.org)\/viewer\.php\?file=(.+)/, s:'http://$1/images/$2', xhr:true},
		{r:/picshd\.com\/([a-z0-9]+)$/i, s:'http://i.picshd.com/$1.jpg'},
		{r:/picsee\.net\/([\d\-]+)\/(.+?)\.html/,s:'http://picsee.net/upload/$1/$2'},
		{r:/picturescream\.com\/\?v=/, q:'#imagen img'},
		{r:/pimpandhost\.com\/(image|guest)\//, q:'#image'},
		{r:/pixhost\.org\/show\//, q:'#show_image', xhr:true},
		{r:/pixhub\.eu\/images/, q:'.image-show img', xhr:true},
		{r:/pixroute\.com\/.+\.html$/, q:'img[id]', xhr:true},
		{r:/postimage\.org\/image\//, q:'center img'},
		{r:/(qkme\.me|quickmeme\.com\/meme)\/([a-z0-9]+)/i, s:'http://i.qkme.me/$2.jpg'},
		{r:/radikal\.ru\/.+\.html$/, q:'div > div > img'},
		{r:/screenlist\.ru\/details/, q:'#picture'},
		{r:/sharenxs\.com\/view\//, q:'#img1', xhr:true},
		{r:/skrinshot\.ru\/view\.php\?img=(\d+)/, s:'http://skrinshot.ru/files/$1.jpg'},
		{r:/image\.skins\.be\/[0-9]+\//, q:'#wallpaper_image'},
		{r:/stooorage\.com\/show\//, q:'#page_body div div img', xhr:true},
		{r:/swagirl\.com\/host\/view/, q:'img.img_full_screen'},
		{r:/(swoopic\.com|imgproof\.net)\/img-/, q:'img.centred_resized', xhr:true},
		{r:/turboimagehost\.com\/p\//, q:'#imageid', xhr:true},
		{r:/(([a-z0-9]+\.twimg\.com|twimg.*?\.akamaihd\.net)\/profile_images\/.+?)_[a-z_-]+(\..+)/i, s:'https://$1$3'},
		{r:/([a-z0-9]+\.twimg\.com\/media\/[a-z0-9_-]+\.(jpe?g|png|gif))/i, s:'http://$1:large'},
		{r:/twimg\.com\/1\/proxy.+?t=(.+?)&/i, s:function(m) { return wn.atob(m[1]).match(/http.+/); }},
		{r:/twitpic\.com(\/show\/[a-z]+)?\/([a-z0-9]+)($|#)/i, s:'http://twitpic.com/show/large/$2'},
		{r:/twitter\.com\/.+\/status\/.+\/photo\//, q:'img.large'},
		{r:/(upix\.me\/files\/.+\/)#(.+)/, s:'http://$1$2'},
		{r:/uppix\.net\/([0-9a-z\/]+)\.html$/i, s:'http://uppix.net/$1.jpg'},
		{r:/(upload\.wikimedia\.org\/wikipedia\/[a-z]+\/)thumb\/([a-z0-9]+\/[a-z0-9]+\/.+?\.(jpe?g|gif|png|svg))/i, s:'http://$1$2'},
		{r:/(userserve-ak\.last\.fm\/serve\/).+?(\/\d+)/, s:'http://$1_$2/0.jpg', html:true},
		{r:/winimg\.com\/view/, q:'#image_container img'},
		{r:/yfrog\.com\/(z\/)?[a-z0-9]+$/i, q:'#main_image, #the-image img'},
		{r:/\/\/[^\?:]+\.(jpe?g|gif|png|svg)($|\?)/i}
	];
	if(cfg.hosts) {
		var lines = cfg.hosts.split(/,?[\r\n\t]+/);
		for(var i = lines.length, s; i-- && (s = lines[i]);) {
			if(!s) continue;
			try {
				var h = JSON.parse(s);
				if(!h || !h.r) throw 'property r missing';
				h.r = new RegExp(h.r, 'i');
				if(h.s && h.s.indexOf('return ') > -1) h.s = new Function('m', 'node', h.s);
				if(h.q && h.q.indexOf('return ') > -1) h.q = new Function('text', h.q);
				hosts.splice(0, 0, h);
			} catch(ex) {
				GM_log('Invalid host: ' + s + '\nReason: ' + ex);
			}
		}
	}
	return hosts;
}

function onMouseOver(e) {
	if(e.shiftKey || cur.zoom || !activate(e.target)) return;
	cur.cx = e.clientX;
	cur.cy = e.clientY;
	if(cfg.key)
		if(e.ctrlKey)
			startPopup();
		else
			setZoomCursor(cur.node);
	else
		cur.timeout = wn.setTimeout(startPopup, cfg.delay);
}

function onMouseMove(e) {
	if(e.shiftKey) return;
	cur.cx = e.clientX;
	cur.cy = e.clientY;
	var r = cur.rect;
	if(!cur.zoomed && (cur.cx > r.right + 1 || cur.cx < r.left - 1 || cur.cy > r.bottom + 1 || cur.cy < r.top - 1)) return deactivate();
	placeStatus();
	if(cur.zoom) placePopup();
}

function onMouseDown(e) {
	if(e.which != 3 && !e.shiftKey) deactivate(true);
}

function onMouseOut(e) {
	if(!e.relatedTarget) deactivate();
}

function onMouseScroll(e) {
	var dir = e.detail || -e.wheelDelta;
	if(cur.zoom) {
		e.preventDefault();
		cur.scale *= dir > 0 ? 0.5 : 2;
		if(cur.scale < cur.minScale) {
			if(cur.gItems) {
				cur.zoom = false;
			} else {
				return deactivate(true);
			}
		}
		placePopup();
		setTitle();
	} else if(cur.gItems && getPopup()) {
		e.preventDefault();
		nextGalleryItem(dir);
	} else if(cfg.zoom == 'wheel' && dir < 0 && getPopup()) {
		e.preventDefault();
		toggleZoom();
	} else {
		deactivate();
	}
}

function onKeyDown(e) {
	if(e.keyCode == 17 && cfg.key && !getPopup()) {
		cur.node.style.cursor = '';
		startPopup();
	}
}

function onKeyUp(e) {
	switch(e.keyCode) {
		case 16:
			toggleZoom();
			break;
		case 17:
			if(!cfg.key) deactivate(true);
			break;
		case 27:
			if(e.shiftKey) {
				off(d.body, 'mouseover', onMouseOver);
				deactivate();
			} else {
				deactivate(true);
			}
			break;
		case 74:
			e.preventDefault();
			nextGalleryItem(1);
			break;
		case 75:
			e.preventDefault();
			nextGalleryItem(-1);
			break;
		case 84:
			GM_openInTab(getPopup().src);
			break;
		default:
			deactivate(true);
	}
}

function onContext(e) {
	if(e.shiftKey) return;
	if(cfg.zoom == 'context' && getPopup() && !getStatus() && toggleZoom())
		e.preventDefault();
	else
		deactivate();
}

function startPopup() {
	if(cur.g)
		startGalleryPopup();
	else
		startSinglePopup(cur.url);
}

function startSinglePopup(url) {
	if(cur.g) setPopup(false);
	setStatus(cur.xhr ? 'xhr' : 'loading');
	if(cur.q) {
		downloadPage(url, cur.post, function(text) {
			var iurl = parseHtml(text, cur.q, url);
			if(!iurl) throw 'Image URL not found in node: ' + cur.q;
			if(hasAcceptableSize(cur.node, iurl)) return setStatus(false);
			if(cur.xhr) downloadImage(iurl, url); else setPopup(iurl);
		});
	} else {
		if(cur.xhr) {
			downloadImage(url, cur.url);
		} else {
			setPopup(url);
		}
	}
}

function startGalleryPopup() {
	setStatus('loading');
	downloadPage(cur.url, cur.post, function(text) {
		cur.gItems = parseGallery(text, cur.g.entry, cur.g.image, cur.g.caption);
		cur.gIndex = -1;
		if(cur.gItems) {
			nextGalleryItem(1);
		} else {
			GM_log('Empty gallery: ' + cur.url);
			deactivate();
		}
	});
}

function nextGalleryItem(dir) {
	if(dir > 0 && ++cur.gIndex >= cur.gItems.length) {
		cur.gIndex = 0;
	} else if(dir < 0 && --cur.gIndex < 0) {
		cur.gIndex = cur.gItems.length - 1;
	}
	var item = cur.gItems[cur.gIndex];
	startSinglePopup(item.url);
}

function activate(node) {
	if(node.id == 'mpiv-popup') return false;
	var info = parseNode(node);
	if(!info.url || info.url == cur.url || hasAcceptableSize(node, info.url)) return false;
	deactivate();
	cur = info;
	var largest = node, nodes = node.querySelectorAll('*');
	for(var i = nodes.length, n; i-- && (n = nodes[i]);) {
		if(!largest || n.clientHeight > largest.clientHeight)
			largest = n;
	}
	var quirks = d.compatMode == 'BackCompat';
	cur.node = node;
	cur.rect = largest.getBoundingClientRect();
	cur.cw = quirks ? d.body.clientWidth  : d.documentElement.clientWidth;
	cur.ch = quirks ? d.body.clientHeight : d.documentElement.clientHeight;
	on(d, 'mousemove', onMouseMove);
	on(d, 'mousedown', onMouseDown);
	on(d, 'contextmenu', onContext);
	on(d, 'keydown', onKeyDown);
	on(d, 'keyup', onKeyUp);
	on(d, 'DOMMouseScroll', onMouseScroll);
	on(d, 'mousewheel', onMouseScroll);
	on(d, 'mouseout', onMouseOut);
	return true;
}

function deactivate(wait) {
	wn.clearTimeout(cur.timeout);
	if(cur.req && typeof cur.req.abort == 'function') cur.req.abort();
	if(cur.node) cur.node.style.cursor = '';
	setTitle(true);
	cur = {};
	off(d, 'mousemove', onMouseMove);
	off(d, 'mousedown', onMouseDown);
	off(d, 'contextmenu', onContext);
	off(d, 'keydown', onKeyDown);
	off(d, 'keyup', onKeyUp);
	off(d, 'DOMMouseScroll', onMouseScroll);
	off(d, 'mousewheel', onMouseScroll);
	off(d, 'mouseout', onMouseOut);
	setStatus(false);
	setPopup(false);
	setCaption(false);
	if(wait) {
		off(d.body, 'mouseover', onMouseOver);
		wn.setTimeout(function() { on(d.body, 'mouseover', onMouseOver); }, 200);
	}
}

function parseNode(node) {
	var info;
	if(node.tagName == 'IMG' && node.src.substr(0, 5) != 'data:') {
		var src = rel2abs(node.src, location.href);
		info = findInfo([src], node);
		if(info && info.url != src) return info;
	}
	if(node.parentNode.tagName == 'A') node = node.parentNode; else if(node.parentNode.parentNode.tagName == 'A') node = node.parentNode.parentNode;
	if(node.tagName == 'A') {
		if(cfg.thumbsonly && !node.querySelector('img, i') && !hasBg(node) && !hasBg(node.parentNode) && !hasBg(node.firstElementChild)) return false;
		info = findInfo(parseUrls(decodeURIComponent(node.href)), node);
		if(info) return info;
	}
	if(cfg.img && (node.tagName == 'IMG' || node.tagName == 'A' && (node = node.querySelector('img')))) {
		return {url: node.src};
	}
	return false;
}

function findInfo(urls, node) {
	var html = node.outerHTML;
	if(!hosts) hosts = loadHosts();
	for(var i = 0, len = hosts.length, h, m; i < len && (h = hosts[i]); i++) {
		if(!(m = h.html ? h.r.exec(html) : findMatch(urls, h.r))) continue;
		var info = {
			url: h.hasOwnProperty('s') ? (typeof h.s == 'function' ? h.s(m, node) : replace(h.s, m)) : m.input,
			q: h.q,
			g: h.g,
			xhr: h.xhr,
			post: h.post
		};
		if(info.url === false) continue;
		return info;
	};
	return false;
}

function downloadPage(url, post, cb) {
	var opts = {
		method: 'GET',
		url: url,
		ignoreCache: true,
		onload: function(req) {
			try {
				delete cur.req;
				cb(req.responseText);
			} catch(ex) {
				showError(ex);
			}
		},
		onerror: showError
	};
	if(post) {
		opts.method = 'POST';
		opts.data = post;
		opts.headers = {'Content-Type':'application/x-www-form-urlencoded','Referer':url};
	}
	cur.req = GM_xmlhttpRequest(opts);
}

function downloadImage(url, referer) {
	cur.req = GM_xmlhttpRequest({
		method: 'GET',
		url: url,
		overrideMimeType: 'text/plain; charset=x-user-defined',
		headers: {'Accept':'image/png,image/*;q=0.8,*/*;q=0.5','Referer':referer},
		onprogress: function(e) {
			if(e.lengthComputable) {
				var per = parseInt(e.loaded / e.total * 100, 10) + '%';
				getStatus().style.background = 'linear-gradient(to right, #bdd6ee ' + per + ', white ' + per + ') padding-box';
			}
		},
		onload: function(req) {
			try {
				delete cur.req;
				var txt = req.responseText, ui8 = new Uint8Array(txt.length);
				for(var i = txt.length; i--;) {
					ui8[i] = txt.charCodeAt(i);
				}
				var b = new Blob([ui8.buffer]);
				var u = wn.URL || wn.webkitURL;
				if(u) {
					setPopup(u.createObjectURL(b));
				} else {
					var fr = new FileReader();
					fr.onload = function() { setPopup(fr.result); };
					fr.onerror = showError;
					fr.readAsDataURL(b);
				}
			} catch(ex) {
				showError(ex);
			}
		},
		onerror: showError
	});
}

function parseHtml(html, q, url) {
	if(typeof q == 'function') return q(html);
	var node, path, doc = d.implementation.createHTMLDocument('MPIV');
	doc.documentElement.innerHTML = html;
	if(typeof q == 'string') {
		node = doc.querySelector(q);
	} else {
		for(var i = 0, len = q.length; i < len; i++) {
			node = doc.querySelector(q[i]);
			if(node) break;
		}
	}
	if(!node) throw 'Node not found: ' + q + '\nPage: ' + url;
	switch(node.tagName) {
		case 'IMG':
			path = node.getAttribute('src').trim();
			break;
		case 'A':
			path = node.getAttribute('href').trim();
			break;
		default:
			path = node.outerHTML.match(/https?:\/\/[.\/a-z0-9_+%\-]+\.(jpe?g|gif|png|svg)/i)[0];
	}
	return rel2abs(path, html.match(/<base[^>]+href=["']([^>]+)["']/i) ? RegExp.$1 : url);
}

function parseGallery(html, qE, qI, qC) {
	var doc = d.implementation.createHTMLDocument('MPIV');
	doc.documentElement.innerHTML = html;
	var nodes = doc.querySelectorAll(qE);
	var items = [];
	for(var i = 0, node, len = nodes.length; i < len && (node = nodes[i]); i++) {
		var item = {url:node.querySelector(qI).src};
		try {
			item.desc = node.querySelector(qC).textContent;
		} catch(ex) {}
		items.push(item);
	}
	return items;
}

function checkProgress(start) {
	if(start === true) {
		wn.clearInterval(checkProgress.interval);
		checkProgress.interval = wn.setInterval(checkProgress, 150);
		return;
	}
	var p = getPopup();
	if(!p) return wn.clearInterval(checkProgress.interval);
	if(p.naturalHeight) {
		setStatus(false);
		wn.clearInterval(checkProgress.interval);
		p.style.display = '';
		placePopup();
		setTitle();
		cur.large = p.naturalWidth > p.clientWidth + cur.mbw || p.naturalHeight > p.clientHeight + cur.mbh;
		if(cur.large) {
			setZoomCursor(p);
			setZoomCursor(cur.node);
		}
		if(cur.gItems) {
			var item = cur.gItems[cur.gIndex];
			setCaption('[' + (cur.gIndex + 1) + '/' + cur.gItems.length + ']' + (item.desc ? ' ' + item.desc : ''));
		}
	}
}

function setZoomCursor(node) {
	node.style.cursor = 'url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABD5JREFUeNqclFtsVFUUhr99LkPn0jKd0kIJVCgRWlvAxmCUVohBTSQ8QIxGYySoPOiDgZj4YqI+GR950AQeSNBAJDyYoDEkYqS0BhWo1laowlCYIqWdmdKZ6Uzndm6umSFRLi+yk5U5s8/a//nXv/61FXwIFODFJ2FZB2QznaRKz1FUz+OqHpQ3iekep9E3QLBhACcJx05DypFzBncv2cnJzzwoW8CcfcTtvQ890sTTHSFagjq5stdyfqLQc354VtLK39LivcFcTlAlH/MeQMXD+2v7y1ec9LcvffbAy63s3LSo+uq/a2BsjneOTvLHSHyayavryBaT92OoeHdYmKl9Ptvd+/vHa+hsDVRf3MzKdyRfedAoW3p112P9+2OMTpQu0mp043r3Adx9vp2p4viRjzp5dUMTsbQrYIqAT74vaD5D4brQsACW1iviOYvWt4bxNG8nPv3w3YAaqfkdy9Y3CliYXMnlzxmPSFBh2S7HziWZnbeJhBRTOY9UwWVxyOSlLYvhVuGVmvb5O0Jj3tu2rStEpaiRaYu85XIjVeZKokAiaxFNFJm4ZVN2XK6lnSqLHV1Boa8/IWeCFU41QWphYJiPR0ypCYt0yRNGFieGU1KmRzigM3gpwy/jWbY+GqG+rqZk2Cfa+YxGlLlKRBu90zaOezXv0A0OqbzFXFmxYVWItJQ6Npmnc2mAtqYFsu9WWVZkr1SB7Vp49tS9Gta535y6VpTHEmsaLG5kHQJ+g4UhQ2zn0CAsF4ofY1Jug2ZVK+mPil62cwIq1ilRC8HwXAFs9H89OpThx4vzbGjXWBko0X+txETGobMtRLLoVv8HBWzTQw5z6TyfnhScgHkJ5ReQutshfVBFUXHLnkkRZHP/5cLKt/vq6F0lHc6VmSl4uJqGrlzWhm3e7PbQ5dxPk34uJh1uTmTXUmdMoWsjtcaIr7SYCNJ3VJTUwvhbLnf3NDd/tTvM6pUyOo7HvFglGNCqk5SJ2wzeCjKaNlndpHHwuxuc7J8W15u7xLBf4FYAo8Lw+gqIXSnSs+TLRKGp97NTqWVjcY9M1uam2OXXv8scGMiz+2CSs3GHZ7rqGZ+12NgRxjYV0b/mtmMKNc0YQc1WBvaDf2+b5Z0wk3mdlLUV3XwKn1qM7RXF5d/jV1flPtjT29vMC31LuJQss7p5ASfOxvlhMAH1wdcwx44YtUvgdrgSpnOIiHsITZQ2jHZsawbXTqAqqf6hM2cSh326xubHFnHuep6N65rIyAQNDc8dxqiPiVM3U72KupbLwDZDudJ+Ma6m2+h6Uqwg8yW+U5KqjFF8Wiw2nt3uirQdbQEuTJdYuyJE1pJGTvv6NP7PqhSiSQOCxq7BnxOcHpqhtUHHL15taRQLuFabwYOsCqhcEr9dSH+elQlqjfg4Ny5md539DwZYZSugpopFo5lPopbXTMQ4Tkl/7x8BBgA15NXR6NAotQAAAABJRU5ErkJggg=="), all-scroll';
}

function placePopup() {
	var p = getPopup();
	if(!p) return;
	if(typeof cur.pw == 'undefined') {
		var s = wn.getComputedStyle(p);
		cur.pw = styleSum(s, ['padding-left', 'padding-right']);
		cur.ph = styleSum(s, ['padding-top', 'padding-bottom']);
		cur.mbw = styleSum(s, ['margin-left', 'margin-right', 'border-left-width', 'border-right-width']);
		cur.mbh = styleSum(s, ['margin-top', 'margin-bottom', 'border-top-width', 'border-bottom-width']);
	}
	var cw = cur.cw, ch = cur.ch;
	if(cur.zoom || cur.gItems) {
		if(cur.gItems && !cur.zoom) cur.scale = Math.min(1, scale(p, false));
		var cx = cur.cx, cy = cur.cy, nw = cur.scale * p.naturalWidth, nh = cur.scale * p.naturalHeight;
		p.style.maxWidth  = 'none';
		p.style.maxHeight = 'none';
		p.style.width  = nw + 'px';
		p.style.height = nh + 'px';
		p.style.left = Math.round((cw > nw ? cw/2 - nw/2 : -1 * Math.min(1, Math.max(0, 5/3*(cx/cw-0.2))) * (nw - cw)) - (cur.pw + cur.mbw)/2) + 'px';
		p.style.top  = Math.round((ch > nh ? ch/2 - nh/2 : -1 * Math.min(1, Math.max(0, 5/3*(cy/ch-0.2))) * (nh - ch)) - (cur.ph + cur.mbh)/2) + 'px';
	} else {
		var r = cur.rect, rx = (r.left + r.right) / 2, ry = (r.top + r.bottom) / 2;
		p.style.maxWidth  = cw - cur.pw - cur.mbw + 'px';
		p.style.maxHeight = ch - cur.ph - cur.mbh + 'px';
		p.style.width  = 'auto';
		p.style.height = 'auto';
		var w = p.clientWidth + cur.mbw, h = p.clientHeight + cur.mbh;
		var x =  Math.min(cw - w, Math.max(0, r.left + (w && rx > cw/2 ? -w -20 : r.width  + 20)));
		var y =  Math.min(ch - h, Math.max(0, r.top  + (h && ry > ch/2 ? -h -20 : r.height + 20)));
		if(h < ch - 80 && (x > r.right || x + w < r.left)) {
			y = Math.min(Math.max(ry - h/2, 40), ch - h - 40);
		} else if(w < cw - 80 && (y > r.bottom || y + h < r.bottom)) {
			x = Math.min(Math.max(rx - w/2, 40), cw - w - 40);
		}
		p.style.left = x + 'px';
		p.style.top  = y + 'px';
	}
}

function placeStatus() {
	var s = getStatus();
	if(s) {
		s.style.left = cur.cx + 'px';
		s.style.top  = cur.cy + 'px';
	}
}

function toggleZoom() {
	var p = getPopup();
	if(!p || !p.naturalHeight) return;
	p.style.cursor = '';
	cur.node.style.cursor = '';
	cur.zoom = !cur.zoom;
	cur.zoomed = true;
	cur.scale = cur.minScale = scale(p, cur.large);
	placePopup();
	setTitle();
	return cur.zoom;
}

function showError(o) {
	setStatus('error');
	if(!o.responseText && !o.target) GM_log(o);
}

function getStatus() {
	return $('mpiv-status');
}

function setStatus(status) {
	var s = getStatus();
	if(s) s.parentNode.removeChild(s);
	if(!status) return;
	var svg = status == 'error' ? '<svg xmlns="http://www.w3.org/2000/svg" xmlns:x="http://www.w3.org/1999/xlink" viewBox="0 0 100 100"><g><polygon id="p" points="43,5 57,5 57,43 95,43 95,57 57,57 57,95 43,95 43,57 5,57 5,43 43,43" transform="rotate(43 50 50)" style="fill:#a20e11;stroke-width:3;stroke:#990000"/></g></svg>' : '<svg xmlns="http://www.w3.org/2000/svg" xmlns:x="http://www.w3.org/1999/xlink" viewBox="0 0 100 100"><defs><style>.r {fill:#9dbbc8;stroke-width:0.3;stroke:gray} .a {fill:#3b5059;stroke-width:0} .b {fill:#4c6772;stroke-width:0} .c {fill:#648896;stroke-width:0} .d {fill:#78a2b3; stroke-width:0;}</style><rect id="r" x="45.5" y="9" width="7" height="23" rx="3" ry="3"/><filter id="f"><feColorMatrix type="saturate" values="0"/></filter></defs><g filter=""><use x:href="#r" class="r"/><use x:href="#r" class="r" transform="rotate(30 50 50)"/><use x:href="#r" class="r" transform="rotate(60 50 50)"/><use x:href="#r" class="r" transform="rotate(90 50 50)"/><use x:href="#r" class="r" transform="rotate(120 50 50)"/><use x:href="#r" class="r" transform="rotate(150 50 50)"/><use x:href="#r" class="r" transform="rotate(180 50 50)"/><use x:href="#r" class="r" transform="rotate(210 50 50)"/><use x:href="#r" class="r" transform="rotate(240 50 50)"/><use x:href="#r" class="r" transform="rotate(270 50 50)"/><use x:href="#r" class="r" transform="rotate(300 50 50)"/><use x:href="#r" class="r" transform="rotate(330 50 50)"/><g><animateTransform attributeName="transform" type="rotate" values="0 50 50; 30 50 50; 60 50 50; 90 50 50; 120 50 50; 150 50 50; 180 50 50; 210 50 50; 240 50 50; 270 50 50; 300 50 50; 330 50 50" dur="1s" repeatCount="indefinite" calcMode="discrete"/><use x:href="#r" class="a" transform="rotate(30 50 50)"/><use x:href="#r" class="b" transform="rotate(0 50 50)"/><use x:href="#r" class="c" transform="rotate(330 50 50)"/><use x:href="#r" class="d" transform="rotate(300 50 50)"/></g></g></svg>';
	if(status == 'xhr') svg = svg.replace('""', '"url(#f)"').replace('1s', '3s');
	s = d.createElement('div');
	s.id = 'mpiv-status';
	s.setAttribute('status', status);
	s.style.cssText = 'position:fixed;z-index:2147483647;left:' + cur.cx + 'px;top:' + cur.cy + 'px;height:40px;width:40px;margin:20px 0 0 20px;padding:0;background:white padding-box;border:1px solid gray;border-radius:8px';
	s.innerHTML = '<img style="border:0;margin:0" src="data:image/svg+xml;base64,' + wn.btoa(svg) + '">';
	d.body.appendChild(s);
}

function getPopup() {
	return $('mpiv-popup');
}

function setPopup(src) {
	var s = getPopup();
	if(!s && !src) return;
	if(!s) {
		s = d.createElement('img');
		s.id = 'mpiv-popup';
		s.style.cssText = 'display:none;border:1px solid gray;background-color:white;position:fixed;z-index:2147483647;margin:0;cursor:default;' + cfg.css;
		on(s, 'error', showError);
		d.body.appendChild(s);
	}
	if(src) {
		s.src = src;
		s.style.display = 'none';
		checkProgress(true);
	} else {
		cur.zoom = false;
		off(s, 'error', showError);
		s.parentNode.removeChild(s);
		if(cur.node) cur.node.style.cursor = '';
	}
}

function getCaption() {
	return $('mpiv-caption');
}

function setCaption(caption) {
	var c = getCaption();
	if(!caption) {
		if(c) c.parentNode.removeChild(c);
		return
	}
	if(!c) {
		c = d.createElement('div');
		c.id = 'mpiv-caption';
		c.style.cssText = 'position:fixed;z-index:2147483647;left:0;right:0;top:-50px;transition:top 500ms;padding:0;text-align:center;font-family:sans-serif;font-size:15px;font-weight:bold;background:rgba(0, 0, 0, 0.6);color:white;padding:4px 10px';
		wn.setTimeout(function() { c.style.top = '0px'; }, 500);
	}
	d.body.appendChild(c);
	c.textContent = caption;
}

function setTitle(reset) {
	if(reset) {
		if(cur.title) d.title = cur.title;
	} else {
		if(!cur.title) cur.title = d.title;
		var p = getPopup();
		d.title = p.naturalWidth + 'x' + p.naturalHeight + ' @ ' + Math.round((p.clientHeight - cur.ph) / p.naturalHeight * 100) + '%';
	}
}

function parseUrls(url) {
	var end = url.length - 1, urls = [];
	if(url.charAt(end) == '#') return urls;
	while(true) {
		var pos = url.lastIndexOf('http', end);
		if(pos === 0 && urls.length === 0) {
			urls.push(url);
			break;
		}
		if(pos == -1) break;
		if(/https?:\/\/[^&]+/.exec(url.substring(pos, end + 1))) {
			urls.push(RegExp.lastMatch);
		}
		if(pos === 0) break;
		end = pos - 1;
	}
	return urls;
}

function findMatch(a, re) {
	for(var i = a.length; i--;) {
		var m = re.exec(a[i]);
		if(m) return m;
	}
	return false;
}

function rel2abs(rel, abs) {
	if(rel.indexOf('//') === 0) rel = 'http:' + rel;
	var re = /^([a-z]+:)?\/\//;
	if(re.test(rel))  return rel;
	if(!re.exec(abs)) return false;
	if(rel[0] == '/') return abs.substr(0, abs.indexOf('/', RegExp.lastMatch.length)) + rel;
	return abs.substr(0, abs.lastIndexOf('/')) + '/' + rel;
}

function replace(s, m) {
	for(var i = m.length; i--;) {
		s = s.replace('$'+i, m[i]);
	}
	return s;
}

function styleSum(s, p) {
	for(var i = p.length, x = 0; i--;) {
		x += parseInt(s.getPropertyValue(p[i], 10), 10) || 0;
	}
	return x;
}

function scale(p, large) {
	return large ? (p.naturalHeight / cur.ch > 3 && p.naturalWidth > cur.cw || p.naturalWidth / cur.cw > 3 && p.naturalHeight > cur.ch ? 0.5 : 1) : Math.min((cur.cw - cur.mbw)/p.naturalWidth, (cur.ch - cur.mbh)/p.naturalHeight);
}

function hasBg(node) {
	return node ? wn.getComputedStyle(node).getPropertyValue('background-image') != 'none' : false;
}

function hasAcceptableSize(node, url) {
	if(!(node.tagName == 'IMG' || (node = node.querySelector('img')))) return false;
	return node.src == url && (!cfg.img || node.clientHeight >= node.naturalHeight);
}

function on(node, e, f) {
	node.addEventListener(e, f, false);
}

function off(node, e, f) {
	node.removeEventListener(e, f, false);
}

function setup() {
	if($('mpiv-setup')) return;
	GM_addStyle('\
		#mpiv-setup { position:fixed;z-index:2147483647;top:40px;right:40px;padding:20px 30px;background:white;width:550px;border:1px solid black; }\
		#mpiv-setup * { color:black;text-align:left;line-height:normal;font-size:12px;font-family:sans-serif; }\
		#mpiv-setup a { color:black;text-decoration:underline; }\
		#mpiv-setup div { text-align:center;font-weight:bold;font-size:14px; }\
		#mpiv-setup ul { margin:15px 0 15px 0;padding:0;list-style:none;background:white;border:0; }\
		#mpiv-setup input { border:1px solid gray;padding:1px;background:none;position:relative;bottom:-2px; }\
		#mpiv-setup input[type=text] { width:40px; }\
		#mpiv-setup li { margin:0;padding:6px 0;vertical-align:middle;background:white;border:0 }\
		#mpiv-setup p { background:white;color:gray;padding:2px 0; margin:0; }\
		#mpiv-setup textarea { height:100px;width:100%;font-size:11px;font-family:monospace;background:none;border:1px solid gray;padding:1px; }\
		#mpiv-setup #mpiv-setup-css { height:30px; }\
		#mpiv-setup button { width:150px;margin:0 10px;text-align:center; }\
	');
	var div = d.createElement('div');
	div.id = 'mpiv-setup';
	d.body.appendChild(div);
	div.innerHTML = '<div>Mouseover Popup Image Viewer</div><ul><li><input type="checkbox" id="mpiv-setup-thumbsonly"> Allow popup over text-only links (e.g. headlines)</li><li><input type="checkbox" id="mpiv-setup-img"> Allow popup over images that have been scaled down in HTML</li><li>Popup activation: automatically after <input id="mpiv-setup-delay" type="text"/> ms / <input type="checkbox" id="mpiv-setup-key"> manually with ctrl</li><li>Zoom activation: <select><option id="mpiv-setup-shift">shift</option><option id="mpiv-setup-wheel">mouse wheel up or shift</option><option id="mpiv-setup-context">right mouse button or shift</option></select></li><li>Custom CSS for popup image (units in px):<textarea id="mpiv-setup-css" spellcheck="false"></textarea></li><li>Custom host rules (one per line):<p>Format: {"r":"urlpattern", "s":"urlsubstitution", "q":"selector", "xhr":true, "html":true}&nbsp;&nbsp;<a href="http://w9p.co/userscripts/mpiv/host_rules.html" target="_blank">more info...</a></p><textarea id="mpiv-setup-hosts" spellcheck="false"></textarea></li></ul><div><button id="mpiv-setup-ok">OK</button><button id="mpiv-setup-cancel">Cancel</button></div>';
	div = null;
	var close = function() { var div = $('mpiv-setup'); div.parentNode.removeChild(div); };
	on($('mpiv-setup-ok'), 'click', function() {
		var delay = parseInt($('mpiv-setup-delay').value, 10);
		if(!isNaN(delay) && delay >= 0) GM_setValue('delay', cfg.delay = delay);
		GM_setValue('thumbsonly', cfg.thumbsonly = !$('mpiv-setup-thumbsonly').checked);
		GM_setValue('img', cfg.img = !!$('mpiv-setup-img').checked);
		GM_setValue('key', cfg.key = !!$('mpiv-setup-key').checked);
		GM_setValue('zoom', cfg.zoom = $('mpiv-setup-context').selected ? 'context' : ($('mpiv-setup-wheel').selected ? 'wheel' : 'shift'));
		GM_setValue('css', cfg.css = $('mpiv-setup-css').value.trim());
		GM_setValue('hosts', cfg.hosts = $('mpiv-setup-hosts').value.trim());
		hosts = loadHosts();
		close();
	});
	on($('mpiv-setup-cancel'), 'click', close);
	$('mpiv-setup-delay').value = cfg.delay;
	$('mpiv-setup-thumbsonly').checked = !cfg.thumbsonly;
	$('mpiv-setup-img').checked = cfg.img;
	$('mpiv-setup-key').checked = cfg.key;
	$('mpiv-setup-' + cfg.zoom).selected = true;
	$('mpiv-setup-css').value = cfg.css;
	$('mpiv-setup-hosts').value = cfg.hosts;
}

on(d.body, 'mouseover', onMouseOver);
GM_registerMenuCommand('Set up Mouseover Popup Image Viewer', setup);