Improve pixiv thumbnails

Stop pixiv from cropping thumbnails to a square. Use higher resolution thumbnails on Retina displays.

当前为 2019-09-25 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Improve pixiv thumbnails
  3. // @name:ja pixivサムネイルを改善する
  4. // @namespace https://www.kepstin.ca/userscript/
  5. // @license MIT; https://spdx.org/licenses/MIT.html
  6. // @version 20190925.0
  7. // @description Stop pixiv from cropping thumbnails to a square. Use higher resolution thumbnails on Retina displays.
  8. // @description:ja 正方形にトリミングされて表示されるのを防止します。Retinaディスプレイで高解像度のサムネイルを使用します。
  9. // @author Calvin Walton
  10. // @match https://www.pixiv.net/
  11. // @match https://www.pixiv.net/artworks/*
  12. // @match https://www.pixiv.net/bookmark.php*
  13. // @match https://www.pixiv.net/bookmark_add.php*
  14. // @match https://www.pixiv.net/bookmark_new_illust.php*
  15. // @match https://www.pixiv.net/discovery
  16. // @match https://www.pixiv.net/discovery/users
  17. // @match https://www.pixiv.net/en/
  18. // @match https://www.pixiv.net/en/artworks/*
  19. // @match https://www.pixiv.net/history.php*
  20. // @match https://www.pixiv.net/howto*
  21. // @match https://www.pixiv.net/member.php*
  22. // @match https://www.pixiv.net/member_illust.php*
  23. // @match https://www.pixiv.net/new_illust.php*
  24. // @match https://www.pixiv.net/new_illust_r18.php*
  25. // @match https://www.pixiv.net/ranking.php*
  26. // @match https://www.pixiv.net/search.php*
  27. // @match https://www.pixiv.net/stacc*
  28. // @grant none
  29. // ==/UserScript==
  30.  
  31. (function() {
  32. 'use strict';
  33.  
  34. // The src prefix: scheme and domain
  35. let src_prefix = 'https://i.pximg.net';
  36. // The src suffix for thumbnails
  37. let thumb_suffix = '_master1200.jpg';
  38. // A regular expression that matches pixiv thumbnail urls
  39. // Has 3 captures:
  40. // $1: thumbnail width (optional)
  41. // $2: thumbnail height (optional)
  42. // $3: everything in the URL after the thumbnail size up to the image suffix
  43. let src_regexp = /https?:\/\/i\.pximg\.net(?:\/c\/(\d+)x(\d+)(?:_[^\/]*)?)?\/(?:custom-thumb|img-master)\/(.*)_(?:custom|master|square)1200.jpg/;
  44.  
  45. // Create a srcset= attribute on the img, with appropriate dpi scaling values
  46. function imgSrcset(img, size, url_stuff) {
  47. img.srcset = `${src_prefix}/c/150x150/img-master/${url_stuff}${thumb_suffix} ${150 / size}x,
  48. ${src_prefix}/c/240x240/img-master/${url_stuff}${thumb_suffix} ${240 / size}x,
  49. ${src_prefix}/c/360x360_70/img-master/${url_stuff}${thumb_suffix} ${360 / size}x,
  50. ${src_prefix}/c/600x600/img-master/${url_stuff}${thumb_suffix} ${600 / size}x,
  51. ${src_prefix}/img-master/${url_stuff}${thumb_suffix} ${1200 / size}x`;
  52. if (150 / size >= window.devicePixelRatio) {
  53. img.src = `${src_prefix}/c/150x150/img-master/${url_stuff}${thumb_suffix}`;
  54. } else if (240 / size >= window.devicePixelRatio) {
  55. img.src = `${src_prefix}/c/240x240/img-master/${url_stuff}${thumb_suffix}`;
  56. } else if (360 / size >= window.devicePixelRatio) {
  57. img.src = `${src_prefix}/c/360x360_70/img-master/${url_stuff}${thumb_suffix}`;
  58. } else if (600 / size >= window.devicePixelRatio) {
  59. img.src = `${src_prefix}/c/600x600/img-master/${url_stuff}${thumb_suffix}`;
  60. } else { /* 1200 */
  61. img.src = `${src_prefix}/img-master/${url_stuff}${thumb_suffix}`;
  62. }
  63. }
  64.  
  65. function findParentSize(node) {
  66. let size = 0;
  67. let e = node;
  68. while (!size) {
  69. if (e.attributes.width && e.attributes.height) {
  70. let width = +e.attributes.width.value;
  71. let height = +e.attributes.height.value;
  72. if (width && width > size) { size = width; }
  73. if (height && height > size) { size = height; }
  74. return size;
  75. }
  76. if (!e.parentElement) {
  77. console.log("Couldn't find a parent node with size set for", node);
  78. return size;
  79. }
  80. e = e.parentElement;
  81. }
  82. }
  83.  
  84. function handleImg(node) {
  85. if (node.dataset.kepstinThumbnail) { return; }
  86.  
  87. if (!node.src.startsWith(src_prefix)) { node.dataset.kepstinThumbnail = 'skip'; return; }
  88.  
  89. let m = node.src.match(src_regexp);
  90. if (!m) { node.dataset.kepstinThumbnail = 'bad'; return; }
  91.  
  92. let size = findParentSize(node);
  93. imgSrcset(node, size, m[3]);
  94. node.style.objectFit = 'contain';
  95.  
  96. node.dataset.kepstinThumbnail = 'ok';
  97. }
  98.  
  99. function handleLayoutThumbnail(node) {
  100. if (node.dataset.kepstinThumbnail) { return; }
  101.  
  102. // Check for lazy-loaded images, which have a temporary URL
  103. // They'll be updated later when the src is set
  104. if (node.src.startsWith('data:') || node.src.endsWith('transparent.gif')) { return; }
  105.  
  106. if (!node.src.startsWith(src_prefix)) { node.dataset.kepstinThumbnail = 'skip'; return; }
  107.  
  108. let m = node.src.match(src_regexp);
  109. if (!m) { node.dataset.kepstinThumbnail = 'bad'; return; }
  110.  
  111. let size = Math.max(m[1], m[2]);
  112. if (!size) { size = 1200 };
  113. imgSrcset(node, size, m[3]);
  114. node.width = node.style.width = m[1];
  115. node.height = node.style.height = m[2];
  116. node.style.objectFit = 'contain';
  117.  
  118. node.dataset.kepstinThumbnail = 'ok';
  119. }
  120.  
  121. function handleDivBackground(node) {
  122. if (node.dataset.kepstinThumbnail) { return; }
  123.  
  124. // Check for lazy-loaded images
  125. // They'll be updated later when the background image (in style attribute) is set
  126. if (node.classList.contains('js-lazyload') || node.classList.contains('lazyloaded') || node.classList.contains('lazyloading')) { return; }
  127.  
  128. if (node.style.backgroundImage.indexOf(src_prefix) == -1) { node.dataset.kepstinThumbnail = 'skip'; return; }
  129. let m = node.style.backgroundImage.match(src_regexp);
  130. if (!m) { node.dataset.kepstinThumbnail = 'bad'; return; }
  131. let size = Math.max(node.clientWidth, node.clientHeight);
  132.  
  133. let parentNode = node.parentElement;
  134. let img = document.createElement('IMG');
  135. imgSrcset(img, size, m[3]);
  136. img.class = node.class;
  137. img.alt = node.getAttribute('alt');
  138. img.style.width = node.style.width;
  139. img.style.height = node.style.height;
  140. img.style.objectFit = 'contain';
  141. img.dataset.kepstinThumbnail = 'ok';
  142.  
  143. node.replaceWith(img);
  144. }
  145.  
  146. function handleABackground(node) {
  147. if (node.dataset.kepstinThumbnail) { return; }
  148.  
  149. if (node.style.backgroundImage.indexOf(src_prefix) == -1) { node.dataset.kepstinThumbnail = 'skip'; return; }
  150. let m = node.style.backgroundImage.match(src_regexp);
  151. if (!m) { node.dataset.kepstinThumbnail = 'bad'; return; }
  152. let size = Math.max(node.clientWidth, node.clientHeight);
  153.  
  154. let img = document.createElement('IMG');
  155. imgSrcset(img, size, m[3]);
  156. img.alt = '';
  157. img.style.width = '100%';
  158. img.style.height = '100%';
  159. img.style.objectFit = 'contain';
  160. img.style.position = 'absolute';
  161. img.dataset.kepstinThumbnail = 'ok';
  162.  
  163. node.style.backgroundImage = '';
  164. node.prepend(img);
  165. }
  166.  
  167. function onetimeThumbnails(parentNode) {
  168. for (let node of parentNode.querySelectorAll('img')) {
  169. if (node.parentElement.classList.contains('_layout-thumbnail')) {
  170. handleLayoutThumbnail(node);
  171. } else {
  172. handleImg(node);
  173. }
  174. }
  175. for (let node of parentNode.querySelectorAll('div[style*=background-image]')) {
  176. handleDivBackground(node);
  177. }
  178. for (let node of parentNode.querySelectorAll('a[style*=background-image]')) {
  179. handleABackground(node);
  180. }
  181. }
  182.  
  183. function mutationObserverCallback(mutationList, observer) {
  184. for (let mutation of mutationList) {
  185. switch (mutation.type) {
  186. case 'childList':
  187. for (let node of mutation.addedNodes) {
  188. if (node.nodeName == 'IMG') {
  189. handleImg(node);
  190. } else if (node.nodeName == 'DIV') {
  191. if (node.style.backgroundImage) {
  192. handleDivBackground(node);
  193. } else {
  194. onetimeThumbnails(node);
  195. }
  196. } else if (node.nodeName == 'A') {
  197. if (node.style.backgroundImage) {
  198. handleABackground(node);
  199. }
  200. } else if (node.nodeName == 'SECTION' || node.nodeName == 'LI') {
  201. onetimeThumbnails(node);
  202. }
  203. }
  204. break;
  205. case 'attributes':
  206. if (mutation.target.nodeName == 'DIV') {
  207. if (mutation.target.style.backgroundImage) {
  208. handleDivBackground(mutation.target);
  209. }
  210. } else if (mutation.target.nodeName == 'IMG') {
  211. if (mutation.target.parentElement.classList.contains('_layout-thumbnail')) {
  212. handleLayoutThumbnail(mutation.target);
  213. }
  214. }
  215. break;
  216. }
  217. }
  218. }
  219.  
  220. if (!window.kepstinThumbnailObserver) {
  221. onetimeThumbnails(document.firstElementChild);
  222. window.kepstinThumbnailObserver = new MutationObserver(mutationObserverCallback);
  223. window.kepstinThumbnailObserver.observe(document.firstElementChild, { childList: true, subtree: true, attributes: true, attributeFilter: [ 'class', 'src' ] });
  224. }
  225. })();