123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498 |
- /**
- * 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;
- }
- })();
|