/** * Load the full-size versions of resized images based on their "src" * attribute, or their containing link's "href" attribute. Also, make IFRAMEs * take up the entire width of their offset parent (useful for embedded videos * and whatnot). Same goes for the VIDEO elements. * * @title Load full images */ (function fullimg() { /* Create a new IFRAME to get a "clean" Window object, so we can use its * console. Sometimes sites (e.g. Twitter) override console.log and even * the entire console object. "delete console.log" or "delete console" * does not always work, and messing with the prototype seemed more * brittle than this. */ var console = (function () { var iframe = document.getElementById('xxxJanConsole'); if (!iframe) { iframe = document.createElementNS('http://www.w3.org/1999/xhtml', 'iframe'); iframe.id = 'xxxJanConsole'; iframe.style.display = 'none'; (document.body || document.documentElement).appendChild(iframe); } return iframe && iframe.contentWindow && iframe.contentWindow.console || { log: function () {} }; })(); /* Get rid of "width=", "height=" etc. followed by numbers or number pairs * in IMG@src query strings. */ var parameterNames = [ 'width', 'Width', 'height', 'Height', 'maxwidth', 'maxWidth', 'MaxWidth', 'maxheight', 'maxHeight', 'MaxHeight', 'w', 'W', 'h', 'H', 'fit', 'Fit', 'resize', 'reSize', 'Resize', 'size', 'Size' ]; parameterNames.forEach(function (parameterName) { var selector = 'img[src*="?' + parameterName + '="]' + ', img[src*="?"][src*="&' + parameterName + '="]'; /* Match query string parameters (?[…&]name=value[&…]) where the value is * a number (e.g. "width=1200") or a pair of numbers (e.g. * "resize=640x480"). */ var parameterReplacementRegexp = new RegExp('(\\?[^#]*&)?' + parameterName + '=[1-9][0-9]+(?:(?:[xX,*:]|%2[CcAa]|%3[Aa])[1-9][0-9]+)?([^&#]*)'); [].forEach.call(document.querySelectorAll(selector), function (img) { var newSrc = img.src /* Remove the parameter "name=value" pair from the query string. */ .replace(parameterReplacementRegexp, '$1$2') /* Remove trailing "&" from the query string. */ .replace(/(\?[^#]*)&(#.*)?$/, '$1$2') /* Remove empty query strings ("?" not followed by * anything) from the URL. */ .replace(/\?(#.*)?$/, '$1') /* Remove empty fragment identifiers from the URL. */ .replace(/#$/, '') ; changeSrc(img, newSrc, 'found image with parameter "' + parameterName + '" in query string'); }); }); /* Show the original image for Polopoly CMS "generated derivatives". * * Example: * https://sporza.be/polopoly_fs/1.2671026!image/1706320883.jpg_gen/derivatives/landscape670/1706320883.jpg * https://sporza.be/polopoly_fs/1.2671026!image/1706320883.jpg */ [].forEach.call( document.querySelectorAll('img[src*="_gen/derivatives/"]'), function (img) { var matches = img.src.match(/(.*\.(jpe?g|png|gif))_gen.*\.\2(\?.*)?$/); if (matches && matches[1]) { changeSrc(img, matches[1], 'found image with Polopoly CMS "generated derivative" URL'); } } ); /* Try to load the originals for images whose source URLs look like * thumbnail/resized versions with dimensions. */ [].forEach.call( document.images, function (img) { var oldSrc = img.src; /* Example: * https://www.cycling-challenge.com/wp-content/uploads/2014/08/IMG_6197-150x150.jpg * https://www.cycling-challenge.com/wp-content/uploads/2014/08/IMG_6197.jpg */ var matches = oldSrc.match(/(.*)[-_.@]\d+x\d+(\.[^\/.]+)/); if (matches && matches[1] && matches[2]) { var newSrc = matches[1] + matches[2]; return changeSrc(img, newSrc, 'found image whose URL looks like a thumbnail/resized version'); } /* Example: * https://upload.wikimedia.org/wikipedia/commons/thumb/8/83/Kowloon-Walled-City-1898.jpg/220px-Kowloon-Walled-City-1898.jpg * https://upload.wikimedia.org/wikipedia/commons/8/83/Kowloon-Walled-City-1898.jpg */ matches = oldSrc.match(/(.*\/)thumb\/(.*)\/[^\/]+$/); if (matches) { var newSrc = matches[1] + matches[2]; return changeSrc(img, newSrc, 'found image whose URL looks like a MediaWiki thumbnail/resized version'); } } ); /* Try to load the originals for images whose source URLs look like * thumbnail/resized versions with a text label. * * Example: * https://www.crazyguyonabike.com/pics/docs/00/01/27/84/small/DSCF3555.JPG * https://www.crazyguyonabike.com/pics/docs/00/01/27/84/large/DSCF3555.JPG */ var thumbnailPathRegexp = /(.*[/.-])(small|thumb|thumbnail|resized|preview|medium)([/.-].*)/; var fullSizePathParts = [ 'large', 'original', 'source', 'normal', 'xlarge', ]; [].forEach.call( document.images, function (img) { var oldSrc = img.src; var matches = oldSrc.match(thumbnailPathRegexp); if (matches) { var newSources = []; fullSizePathParts.forEach(function (part) { newSources.push(matches[1] + part + matches[3]); }); changeSrc(img, newSources, 'found image whose URL looks like a thumbnail/resized version'); } } ); /* Change the IMG@src of linked images to their link's A@href if they look * similar, assuming that the linked version is larger. */ [].forEach.call( document.querySelectorAll('a img'), function (img) { if (!img.src) { return; } var a = img.parentNode; while (a && a.tagName && a.tagName.toLowerCase() !== 'a') { a = a.parentNode; } if (!a) { return; } var aHref = a.href; if (a.hostname.match(/\.blogspot\.com$/)) { /* Get rid of Blogspot's links to useless HTML wrappers. */ aHref = aHref.replace(/\/(s\d+)-h\/([^\/]+)$/, '/$1/$2'); } if (aHref === img.src) { return; } /* Simplify a URL for similarity calculation. */ function simplifyUrl(url) { return ('' + url) .replace(/\d+/g, '0') .replace(/^https?:/, ''); } var similarity = getSimilarity(simplifyUrl(img.src), simplifyUrl(a.href)); if (similarity > 0.66) { changeSrc(img, aHref, 'found linked image with ' + Math.round(similarity * 100) + '% similarity'); } } ); /* Change all Blogspot images that have not been changed yet. */ Array.from( document.querySelectorAll('img[src*="bp.blogspot.com/"]') ).forEach(img => { let matches; if ((matches = img.src.match(/^(.*\/)s(\d+)(\/[^/]+)$/)) && matches[2] < 9999) { let newSrc = matches[1] + 's9999' + matches[3]; changeSrc(img, newSrc, 'found Blogspot image with restricted size (' + matches[2] + ')'); } }); /* Use larger YouTube thumbnails. */ Array.from( document.querySelectorAll('img[src*="//yt"][src*=".ggpht.com"]') ).forEach(img => { let matches; if ((matches = img.src.match(/^(.*\/)s(\d+)([^/]+\/photo\.[^/.]+)$/)) && matches[2] < 1024) { let newSrc = matches[1] + 's1024' + matches[3]; changeSrc(img, newSrc, 'found YouTube avatar with restricted size (' + matches[2] + ')'); } }); /* Get rid of all IMG@srcset attributes that have not been removed in the * previous steps. */ [].forEach.call( document.querySelectorAll('img[srcset]'), function (img) { console.log('Load full images: removing srcset attribute: ', img); img.originalSrcset = img.getAttribute('srcset'); img.removeAttribute('srcset'); } ); /* Make native VIDEO elements and video IFRAMEs take up the entire width * of their offset parent. */ var elementsToEnlargeSelectors = [ 'video', 'iframe.twitter-tweet-rendered', 'iframe[src*="embed"]', 'iframe[src*="video"]', 'iframe[src*="syndication"]', 'iframe[class*="altura"]', 'iframe[id*="altura"]', 'iframe[src*="altura"]', 'iframe[src*="//e.infogr.am/"]', 'iframe[src*="//www.kickstarter.com/projects/"]', 'iframe[src*="//media-service.vara.nl/player.php"]', 'iframe[src*="//player.vimeo.com/video/"]' ]; [].forEach.call( document.querySelectorAll(elementsToEnlargeSelectors.join(', ')), function (element) { var scale = element.offsetParent.offsetWidth / element.offsetWidth; var newWidth = Math.round(element.offsetWidth * scale); var newHeight = Math.round(element.offsetHeight * scale); console.log( 'Load full images: resizing element ', element, ' from ' + element.offsetWidth + 'x' + element.offsetHeight + ' to ' + newWidth + 'x' + newHeight ); element.xxxJanReadableAllowStyle = true; element.style.width = newWidth + 'px'; element.style.height = newHeight + 'px'; } ); /* Show controls on AUDIO and VIDEO elements. */ [].forEach.call( document.querySelectorAll('audio, video'), function (element) { element.controls = true; } ); /* Show controls on YouTube embeds. */ [].forEach.call( document.querySelectorAll('iframe[src^="https://www.youtube.com/embed/"][src*="?"][src*="=0"]'), function (iframe) { var beforeAndAfterHash = iframe.src.split('#'); var beforeAndAfterQuery = beforeAndAfterHash[0].split('?'); var newPrefix = beforeAndAfterQuery[0]; var newQueryString = ''; if (beforeAndAfterQuery.length > 1) { beforeAndAfterQuery.shift(); var newQueryParts = beforeAndAfterQuery .join('?') .split('&') .filter(function (keyValuePair) { return !keyValuePair.match(/^(controls|showinfo|rel)=0$/); } ); if (newQueryParts.length) { newQueryString = '?' + newQueryParts.join('&'); } } var newHash = ''; if (beforeAndAfterHash.length > 1) { beforeAndAfterHash.shift(); newHash = '#' + beforeAndAfterHash.join('#'); } var newSrc = newPrefix + newQueryString + newHash; if (newSrc !== iframe.src) { iframe.src = newSrc; } } ); /** * Crudely calculate the similarity between two strings. Taken from * https://stackoverflow.com/a/10473855. An alternative would be the * Levenshtein distance, implemented in JavaScript here: * https://andrew.hedges.name/experiments/levenshtein/ */ function getSimilarity(strA, strB) { var result = 0; var i = Math.min(strA.length, strB.length); if (i === 0) { return; } while (--i) { if (strA[i] === strB[i]) { continue; } if (strA[i].toLowerCase() === strB[i].toLowerCase()) { result++; } else { result += 4; } } return 1 - (result + 4 * Math.abs(strA.length - strB.length)) / (2 * (strA.length + strB.length)); } /** * Change the IMG@src and fall back to the original source if the new * source triggers an error. You can specify an array of new sources that * will be tried in order. When all of the new sources fail, the original * source will be used. */ function changeSrc(img, newSrc, reason) { var basename = img.src.replace(/[?#].*/, '').replace(/.*?([^\/]*)\/*$/, '$1'); console.log('[' + basename + '] Load full images: ' + reason + ': ', img); if (img.hasNewSource) { console.log('[' + basename + '] Image already has a new source: ', img); return; } var newSources = Array.isArray(newSrc) ? newSrc : [ newSrc ]; while ((newSrc = newSources.shift())) { if (newSrc && img.src !== newSrc) { break; } } if (!newSrc) { return; } console.log('[' + basename + '] → Old img.src: ' + img.src); console.log('[' + basename + '] → Try img.src: ' + newSrc); /* Save the original source. */ if (!img.originalSrc) { img.originalSrc = img.src; } if (!img.originalNaturalWidth) { img.originalNaturalWidth = img.naturalWidth; } if (!img.originalNaturalHeight) { img.originalNaturalHeight = img.naturalHeight; } /* Save and disable the srcset on the IMG element. */ if (img.hasAttribute('srcset')) { img.originalSrcset = img.getAttribute('srcset'); img.removeAttribute('srcset'); } /* Save and disable the srcset in the container PICTURE element's SOURCE descendants. */ if (img.parentNode.tagName.toLowerCase() === 'picture') { [].forEach.call( img.parentNode.querySelectorAll('source[srcset]'), function (source) { source.originalSrcset = source.getAttribute('srcset'); source.removeAttribute('srcset'); } ); } /* When the new source has failed to load, load the next one from the * list of possible new sources. If there are no more left, revert to * the original source. */ var errorHandler; if (newSources.length) { console.log('[' + basename + '] Setting errorHandler to loadNextNewSrc for ', img, '; newSources: "' + newSources.join('", "') + '"; reason:', reason); errorHandler = function loadNextNewSrc() { img.removeEventListener('error', loadNextNewSrc); changeSrc(img, newSources, reason); }; } else { console.log('[' + basename + '] Setting errorHandler to restoreOriginalSrc for ', img, '; originalSrc: "' + img.originalSrc + '"; reason:', reason); errorHandler = function restoreOriginalSrc() { console.log('[' + basename + '] Load full images: error while loading new source for image: ', img); console.log('[' + basename + '] → Unable to load new img.src: ' + newSrc); console.log('[' + basename + '] → Resetting to original img.src: ' + img.originalSrc); img.removeEventListener('error', restoreOriginalSrc); /* Restore the original source. */ img.src = img.originalSrc; /* Re-enable the original srcset on the IMG element. */ if (img.originalSrcset) { img.setAttribute('srcset', img.originalSrcset); delete img.originalSrcset; } /* Re-enable the original srcset in the container PICTURE element's SOURCE descendants. */ if (img.parentNode.tagName.toLowerCase() === 'picture') { [].forEach.call( img.parentNode.querySelectorAll('source'), function (source) { if (source.originalSrcset) { source.setAttribute('srcset', source.originalSrcset); delete source.originalSrcset; } } ); } }; } img.addEventListener('error', errorHandler); /* When the new source image is smaller than the original image, * treat that as an error, too. */ img.addEventListener('load', function () { if (img.naturalWidth * img.naturalHeight < img.originalNaturalWidth * img.originalNaturalHeight) { console.log('[' + basename + '] Load full images: new image (', img.naturalWidth, 'x', img.naturalHeight, ') is smaller than old image (', img.originalNaturalWidth, 'x', img.originalNaturalHeight, '): ', img); return errorHandler(); } if (img.src !== img.originalSrc) { console.log('[' + basename + '] → Success: ' + img.src); img.hasNewSource = true; } }); /* Finally, actually try to load the image. */ img.src = newSrc; } })();