fullimg.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  1. /**
  2. * Load the full-size versions of resized images based on their "src"
  3. * attribute, or their containing link's "href" attribute. Also, make IFRAMEs
  4. * take up the entire width of their offset parent (useful for embedded videos
  5. * and whatnot). Same goes for the VIDEO elements.
  6. *
  7. * @title Load full images
  8. */
  9. (function fullimg() {
  10. /* Create a new IFRAME to get a "clean" Window object, so we can use its
  11. * console. Sometimes sites (e.g. Twitter) override console.log and even
  12. * the entire console object. "delete console.log" or "delete console"
  13. * does not always work, and messing with the prototype seemed more
  14. * brittle than this. */
  15. var console = (function () {
  16. var iframe = document.getElementById('xxxJanConsole');
  17. if (!iframe) {
  18. iframe = document.createElementNS('http://www.w3.org/1999/xhtml', 'iframe');
  19. iframe.id = 'xxxJanConsole';
  20. iframe.style.display = 'none';
  21. (document.body || document.documentElement).appendChild(iframe);
  22. }
  23. return iframe && iframe.contentWindow && iframe.contentWindow.console || {
  24. log: function () {}
  25. };
  26. })();
  27. /* Get rid of "width=", "height=" etc. followed by numbers or number pairs
  28. * in IMG@src query strings. */
  29. var parameterNames = [
  30. 'width',
  31. 'Width',
  32. 'height',
  33. 'Height',
  34. 'maxwidth',
  35. 'maxWidth',
  36. 'MaxWidth',
  37. 'maxheight',
  38. 'maxHeight',
  39. 'MaxHeight',
  40. 'w',
  41. 'W',
  42. 'h',
  43. 'H',
  44. 'fit',
  45. 'Fit',
  46. 'resize',
  47. 'reSize',
  48. 'Resize',
  49. 'size',
  50. 'Size'
  51. ];
  52. parameterNames.forEach(function (parameterName) {
  53. var selector = 'img[src*="?' + parameterName + '="]'
  54. + ', img[src*="?"][src*="&' + parameterName + '="]';
  55. /* Match query string parameters (?[…&]name=value[&…]) where the value is
  56. * a number (e.g. "width=1200") or a pair of numbers (e.g. * "resize=640x480"). */
  57. var parameterReplacementRegexp = new RegExp('(\\?[^#]*&)?' + parameterName + '=[1-9][0-9]+(?:(?:[xX,*:]|%2[CcAa]|%3[Aa])[1-9][0-9]+)?([^&#]*)');
  58. [].forEach.call(document.querySelectorAll(selector), function (img) {
  59. var newSrc = img.src
  60. /* Remove the parameter "name=value" pair from the query string. */
  61. .replace(parameterReplacementRegexp, '$1$2')
  62. /* Remove trailing "&" from the query string. */
  63. .replace(/(\?[^#]*)&(#.*)?$/, '$1$2')
  64. /* Remove empty query strings ("?" not followed by
  65. * anything) from the URL. */
  66. .replace(/\?(#.*)?$/, '$1')
  67. /* Remove empty fragment identifiers from the URL. */
  68. .replace(/#$/, '')
  69. ;
  70. changeSrc(img, newSrc, 'found image with parameter "' + parameterName + '" in query string');
  71. });
  72. });
  73. /* Show the original image for Polopoly CMS "generated derivatives".
  74. *
  75. * Example:
  76. * https://sporza.be/polopoly_fs/1.2671026!image/1706320883.jpg_gen/derivatives/landscape670/1706320883.jpg
  77. * https://sporza.be/polopoly_fs/1.2671026!image/1706320883.jpg
  78. */
  79. [].forEach.call(
  80. document.querySelectorAll('img[src*="_gen/derivatives/"]'),
  81. function (img) {
  82. var matches = img.src.match(/(.*\.(jpe?g|png|gif))_gen.*\.\2(\?.*)?$/);
  83. if (matches && matches[1]) {
  84. changeSrc(img, matches[1], 'found image with Polopoly CMS "generated derivative" URL');
  85. }
  86. }
  87. );
  88. /* Try to load the originals for images whose source URLs look like
  89. * thumbnail/resized versions with dimensions.
  90. */
  91. [].forEach.call(
  92. document.images,
  93. function (img) {
  94. var oldSrc = img.src;
  95. /* Example:
  96. * https://www.cycling-challenge.com/wp-content/uploads/2014/08/IMG_6197-150x150.jpg
  97. * https://www.cycling-challenge.com/wp-content/uploads/2014/08/IMG_6197.jpg
  98. */
  99. var matches = oldSrc.match(/(.*)[-_.@]\d+x\d+(\.[^\/.]+)/);
  100. if (matches && matches[1] && matches[2]) {
  101. var newSrc = matches[1] + matches[2];
  102. return changeSrc(img, newSrc, 'found image whose URL looks like a thumbnail/resized version');
  103. }
  104. /* Example:
  105. * https://upload.wikimedia.org/wikipedia/commons/thumb/8/83/Kowloon-Walled-City-1898.jpg/220px-Kowloon-Walled-City-1898.jpg
  106. * https://upload.wikimedia.org/wikipedia/commons/8/83/Kowloon-Walled-City-1898.jpg
  107. */
  108. matches = oldSrc.match(/(.*\/)thumb\/(.*)\/[^\/]+$/);
  109. if (matches) {
  110. var newSrc = matches[1] + matches[2];
  111. return changeSrc(img, newSrc, 'found image whose URL looks like a MediaWiki thumbnail/resized version');
  112. }
  113. }
  114. );
  115. /* Try to load the originals for images whose source URLs look like
  116. * thumbnail/resized versions with a text label.
  117. *
  118. * Example:
  119. * https://www.crazyguyonabike.com/pics/docs/00/01/27/84/small/DSCF3555.JPG
  120. * https://www.crazyguyonabike.com/pics/docs/00/01/27/84/large/DSCF3555.JPG
  121. */
  122. var thumbnailPathRegexp = /(.*[/.-])(small|thumb|thumbnail|resized|preview|medium)([/.-].*)/;
  123. var fullSizePathParts = [
  124. 'large',
  125. 'original',
  126. 'source',
  127. 'normal',
  128. 'xlarge',
  129. ];
  130. [].forEach.call(
  131. document.images,
  132. function (img) {
  133. var oldSrc = img.src;
  134. var matches = oldSrc.match(thumbnailPathRegexp);
  135. if (matches) {
  136. var newSources = [];
  137. fullSizePathParts.forEach(function (part) {
  138. newSources.push(matches[1] + part + matches[3]);
  139. });
  140. changeSrc(img, newSources, 'found image whose URL looks like a thumbnail/resized version');
  141. }
  142. }
  143. );
  144. /* Change the IMG@src of linked images to their link's A@href if they look
  145. * similar, assuming that the linked version is larger. */
  146. [].forEach.call(
  147. document.querySelectorAll('a img'),
  148. function (img) {
  149. if (!img.src) {
  150. return;
  151. }
  152. var a = img.parentNode;
  153. while (a && a.tagName && a.tagName.toLowerCase() !== 'a') {
  154. a = a.parentNode;
  155. }
  156. if (!a) {
  157. return;
  158. }
  159. var aHref = a.href;
  160. if (a.hostname.match(/\.blogspot\.com$/)) {
  161. /* Get rid of Blogspot's links to useless HTML wrappers. */
  162. aHref = aHref.replace(/\/(s\d+)-h\/([^\/]+)$/, '/$1/$2');
  163. }
  164. if (aHref === img.src) {
  165. return;
  166. }
  167. /* Simplify a URL for similarity calculation. */
  168. function simplifyUrl(url) {
  169. return ('' + url)
  170. .replace(/\d+/g, '0')
  171. .replace(/^https?:/, '');
  172. }
  173. var similarity = getSimilarity(simplifyUrl(img.src), simplifyUrl(a.href));
  174. if (similarity > 0.66) {
  175. changeSrc(img, aHref, 'found linked image with ' + Math.round(similarity * 100) + '% similarity');
  176. }
  177. }
  178. );
  179. /* Change all Blogspot images that have not been changed yet. */
  180. Array.from(
  181. document.querySelectorAll('img[src*="bp.blogspot.com/"]')
  182. ).forEach(img => {
  183. let matches;
  184. if ((matches = img.src.match(/^(.*\/)s(\d+)(\/[^/]+)$/)) && matches[2] < 9999) {
  185. let newSrc = matches[1] + 's9999' + matches[3];
  186. changeSrc(img, newSrc, 'found Blogspot image with restricted size (' + matches[2] + ')');
  187. }
  188. });
  189. /* Use larger YouTube thumbnails. */
  190. Array.from(
  191. document.querySelectorAll('img[src*="//yt"][src*=".ggpht.com"]')
  192. ).forEach(img => {
  193. let matches;
  194. if ((matches = img.src.match(/^(.*\/)s(\d+)([^/]+\/photo\.[^/.]+)$/)) && matches[2] < 1024) {
  195. let newSrc = matches[1] + 's1024' + matches[3];
  196. changeSrc(img, newSrc, 'found YouTube avatar with restricted size (' + matches[2] + ')');
  197. }
  198. });
  199. /* Get rid of all IMG@srcset attributes that have not been removed in the
  200. * previous steps.
  201. */
  202. [].forEach.call(
  203. document.querySelectorAll('img[srcset]'),
  204. function (img) {
  205. console.log('Load full images: removing srcset attribute: ', img);
  206. img.originalSrcset = img.getAttribute('srcset');
  207. img.removeAttribute('srcset');
  208. }
  209. );
  210. /* Make native VIDEO elements and video IFRAMEs take up the entire width
  211. * of their offset parent. */
  212. var elementsToEnlargeSelectors = [
  213. 'video',
  214. 'iframe.twitter-tweet-rendered',
  215. 'iframe[src*="embed"]',
  216. 'iframe[src*="video"]',
  217. 'iframe[src*="syndication"]',
  218. 'iframe[class*="altura"]',
  219. 'iframe[id*="altura"]',
  220. 'iframe[src*="altura"]',
  221. 'iframe[src*="//e.infogr.am/"]',
  222. 'iframe[src*="//www.kickstarter.com/projects/"]',
  223. 'iframe[src*="//media-service.vara.nl/player.php"]',
  224. 'iframe[src*="//player.vimeo.com/video/"]'
  225. ];
  226. [].forEach.call(
  227. document.querySelectorAll(elementsToEnlargeSelectors.join(', ')),
  228. function (element) {
  229. var scale = element.offsetParent.offsetWidth / element.offsetWidth;
  230. var newWidth = Math.round(element.offsetWidth * scale);
  231. var newHeight = Math.round(element.offsetHeight * scale);
  232. console.log(
  233. 'Load full images: resizing element ', element,
  234. ' from ' + element.offsetWidth + 'x' + element.offsetHeight
  235. + ' to ' + newWidth + 'x' + newHeight
  236. );
  237. element.xxxJanReadableAllowStyle = true;
  238. element.style.width = newWidth + 'px';
  239. element.style.height = newHeight + 'px';
  240. }
  241. );
  242. /* Show controls on AUDIO and VIDEO elements. */
  243. [].forEach.call(
  244. document.querySelectorAll('audio, video'),
  245. function (element) {
  246. element.controls = true;
  247. }
  248. );
  249. /* Show controls on YouTube embeds. */
  250. [].forEach.call(
  251. document.querySelectorAll('iframe[src^="https://www.youtube.com/embed/"][src*="?"][src*="=0"]'),
  252. function (iframe) {
  253. var beforeAndAfterHash = iframe.src.split('#');
  254. var beforeAndAfterQuery = beforeAndAfterHash[0].split('?');
  255. var newPrefix = beforeAndAfterQuery[0];
  256. var newQueryString = '';
  257. if (beforeAndAfterQuery.length > 1) {
  258. beforeAndAfterQuery.shift();
  259. var newQueryParts = beforeAndAfterQuery
  260. .join('?')
  261. .split('&')
  262. .filter(function (keyValuePair) {
  263. return !keyValuePair.match(/^(controls|showinfo|rel)=0$/);
  264. }
  265. );
  266. if (newQueryParts.length) {
  267. newQueryString = '?' + newQueryParts.join('&');
  268. }
  269. }
  270. var newHash = '';
  271. if (beforeAndAfterHash.length > 1) {
  272. beforeAndAfterHash.shift();
  273. newHash = '#' + beforeAndAfterHash.join('#');
  274. }
  275. var newSrc = newPrefix + newQueryString + newHash;
  276. if (newSrc !== iframe.src) {
  277. iframe.src = newSrc;
  278. }
  279. }
  280. );
  281. /**
  282. * Crudely calculate the similarity between two strings. Taken from
  283. * https://stackoverflow.com/a/10473855. An alternative would be the
  284. * Levenshtein distance, implemented in JavaScript here:
  285. * https://andrew.hedges.name/experiments/levenshtein/
  286. */
  287. function getSimilarity(strA, strB) {
  288. var result = 0;
  289. var i = Math.min(strA.length, strB.length);
  290. if (i === 0) {
  291. return;
  292. }
  293. while (--i) {
  294. if (strA[i] === strB[i]) {
  295. continue;
  296. }
  297. if (strA[i].toLowerCase() === strB[i].toLowerCase()) {
  298. result++;
  299. } else {
  300. result += 4;
  301. }
  302. }
  303. return 1 - (result + 4 * Math.abs(strA.length - strB.length)) / (2 * (strA.length + strB.length));
  304. }
  305. /**
  306. * Change the IMG@src and fall back to the original source if the new
  307. * source triggers an error. You can specify an array of new sources that
  308. * will be tried in order. When all of the new sources fail, the original
  309. * source will be used.
  310. */
  311. function changeSrc(img, newSrc, reason)
  312. {
  313. var basename = img.src.replace(/[?#].*/, '').replace(/.*?([^\/]*)\/*$/, '$1');
  314. console.log('[' + basename + '] Load full images: ' + reason + ': ', img);
  315. if (img.hasNewSource) {
  316. console.log('[' + basename + '] Image already has a new source: ', img);
  317. return;
  318. }
  319. var newSources = Array.isArray(newSrc)
  320. ? newSrc
  321. : [ newSrc ];
  322. while ((newSrc = newSources.shift())) {
  323. if (newSrc && img.src !== newSrc) {
  324. break;
  325. }
  326. }
  327. if (!newSrc) {
  328. return;
  329. }
  330. console.log('[' + basename + '] → Old img.src: ' + img.src);
  331. console.log('[' + basename + '] → Try img.src: ' + newSrc);
  332. /* Save the original source. */
  333. if (!img.originalSrc) {
  334. img.originalSrc = img.src;
  335. }
  336. if (!img.originalNaturalWidth) {
  337. img.originalNaturalWidth = img.naturalWidth;
  338. }
  339. if (!img.originalNaturalHeight) {
  340. img.originalNaturalHeight = img.naturalHeight;
  341. }
  342. /* Save and disable the srcset on the IMG element. */
  343. if (img.hasAttribute('srcset')) {
  344. img.originalSrcset = img.getAttribute('srcset');
  345. img.removeAttribute('srcset');
  346. }
  347. /* Save and disable the srcset in the container PICTURE element's SOURCE descendants. */
  348. if (img.parentNode.tagName.toLowerCase() === 'picture') {
  349. [].forEach.call(
  350. img.parentNode.querySelectorAll('source[srcset]'),
  351. function (source) {
  352. source.originalSrcset = source.getAttribute('srcset');
  353. source.removeAttribute('srcset');
  354. }
  355. );
  356. }
  357. /* When the new source has failed to load, load the next one from the
  358. * list of possible new sources. If there are no more left, revert to
  359. * the original source. */
  360. var errorHandler;
  361. if (newSources.length) {
  362. console.log('[' + basename + '] Setting errorHandler to loadNextNewSrc for ', img, '; newSources: "' + newSources.join('", "') + '"; reason:', reason);
  363. errorHandler = function loadNextNewSrc() {
  364. img.removeEventListener('error', loadNextNewSrc);
  365. changeSrc(img, newSources, reason);
  366. };
  367. } else {
  368. console.log('[' + basename + '] Setting errorHandler to restoreOriginalSrc for ', img, '; originalSrc: "' + img.originalSrc + '"; reason:', reason);
  369. errorHandler = function restoreOriginalSrc() {
  370. console.log('[' + basename + '] Load full images: error while loading new source for image: ', img);
  371. console.log('[' + basename + '] → Unable to load new img.src: ' + newSrc);
  372. console.log('[' + basename + '] → Resetting to original img.src: ' + img.originalSrc);
  373. img.removeEventListener('error', restoreOriginalSrc);
  374. /* Restore the original source. */
  375. img.src = img.originalSrc;
  376. /* Re-enable the original srcset on the IMG element. */
  377. if (img.originalSrcset) {
  378. img.setAttribute('srcset', img.originalSrcset);
  379. delete img.originalSrcset;
  380. }
  381. /* Re-enable the original srcset in the container PICTURE element's SOURCE descendants. */
  382. if (img.parentNode.tagName.toLowerCase() === 'picture') {
  383. [].forEach.call(
  384. img.parentNode.querySelectorAll('source'),
  385. function (source) {
  386. if (source.originalSrcset) {
  387. source.setAttribute('srcset', source.originalSrcset);
  388. delete source.originalSrcset;
  389. }
  390. }
  391. );
  392. }
  393. };
  394. }
  395. img.addEventListener('error', errorHandler);
  396. /* When the new source image is smaller than the original image,
  397. * treat that as an error, too. */
  398. img.addEventListener('load', function () {
  399. if (img.naturalWidth * img.naturalHeight < img.originalNaturalWidth * img.originalNaturalHeight) {
  400. console.log('[' + basename + '] Load full images: new image (', img.naturalWidth, 'x', img.naturalHeight, ') is smaller than old image (', img.originalNaturalWidth, 'x', img.originalNaturalHeight, '): ', img);
  401. return errorHandler();
  402. }
  403. if (img.src !== img.originalSrc) {
  404. console.log('[' + basename + '] → Success: ' + img.src);
  405. img.hasNewSource = true;
  406. }
  407. });
  408. /* Finally, actually try to load the image. */
  409. img.src = newSrc;
  410. }
  411. })();