Improve pixiv thumbnails

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

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

  1. // ==UserScript==
  2. // @name Improve pixiv thumbnails
  3. // @name:ja pixivサムネイルを改善する
  4. // @namespace https://www.kepstin.ca/userscript/
  5. // @version 20190912.5
  6. // @description Stop pixiv from cropping thumbnails to a square. Use higher resolution thumbnails on Retina displays.
  7. // @description:ja 正方形にトリミングされて表示されるのを防止します。Retinaディスプレイで高解像度のサムネイルを使用します。
  8. // @author Calvin Walton
  9. // @match https://www.pixiv.net/
  10. // @match https://www.pixiv.net/bookmark.php*
  11. // @match https://www.pixiv.net/bookmark_add.php*
  12. // @match https://www.pixiv.net/bookmark_new_illust.php*
  13. // @match https://www.pixiv.net/discovery
  14. // @match https://www.pixiv.net/member.php*
  15. // @match https://www.pixiv.net/member_illust.php*
  16. // @match https://www.pixiv.net/new_illust.php*
  17. // @match https://www.pixiv.net/new_illust_r18.php*
  18. // @match https://www.pixiv.net/ranking.php*
  19. // @match https://www.pixiv.net/search.php*
  20. // @match https://www.pixiv.net/stacc*
  21. // @grant none
  22. // ==/UserScript==
  23.  
  24. (function() {
  25. 'use strict';
  26.  
  27. // The src prefix: scheme and domain
  28. let src_prefix = 'https://i.pximg.net';
  29. // The src suffix for thumbnails
  30. let thumb_suffix = '_master1200.jpg';
  31. // A regular expression that matches pixiv thumbnail urls
  32. // Has 3 captures:
  33. // $1: thumbnail width (optional)
  34. // $2: thumbnail height (optional)
  35. // $3: everything in the URL after the thumbnail size up to the image suffix
  36. let src_regexp = /https?:\/\/i\.pximg\.net(?:\/c\/(\d+)x(\d+)(?:_[^\/]*)?)?\/(?:custom-thumb|img-master)\/(.*)_(?:custom|master|square)1200.jpg/;
  37.  
  38. // Create a srcset= attribute on the img, with appropriate dpi scaling values
  39. function imgSrcset(img, size, url_stuff) {
  40. if (150 / size >= window.devicePixelRatio) {
  41. img.src = `${src_prefix}/c/150x150/img-master/${url_stuff}${thumb_suffix}`;
  42. } else if (240 / size >= window.devicePixelRatio) {
  43. img.src = `${src_prefix}/c/240x240/img-master/${url_stuff}${thumb_suffix}`;
  44. } else if (360 / size >= window.devicePixelRatio) {
  45. img.src = `${src_prefix}/c/360x360_70/img-master/${url_stuff}${thumb_suffix}`;
  46. } else if (600 / size >= window.devicePixelRatio) {
  47. img.src = `${src_prefix}/c/600x600/img-master/${url_stuff}${thumb_suffix}`;
  48. } else { /* 1200 */
  49. img.src = `${src_prefix}/img-master/${url_stuff}${thumb_suffix}`;
  50. }
  51. img.srcset = `${src_prefix}/c/150x150/img-master/${url_stuff}${thumb_suffix} ${150 / size}x,
  52. ${src_prefix}/c/240x240/img-master/${url_stuff}${thumb_suffix} ${240 / size}x,
  53. ${src_prefix}/c/360x360_70/img-master/${url_stuff}${thumb_suffix} ${360 / size}x,
  54. ${src_prefix}/c/600x600/img-master/${url_stuff}${thumb_suffix} ${600 / size}x,
  55. ${src_prefix}/img-master/${url_stuff}${thumb_suffix} ${1200 / size}x`;
  56. }
  57.  
  58. // Reconfigure a thumbnail set via css background image (usually on a div tag)
  59. function cssBackgroundImage(element) {
  60. // In the future this should use https://developer.mozilla.org/en-US/docs/Web/CSS/image-set
  61. // but right now only chrome/safari have a vendor-prefixed version. Manually pick the image
  62. // based on the devicePixelRatio.
  63. let size = Math.max(element.clientWidth, element.clientHeight);
  64. let m = element.style.backgroundImage.match(src_regexp);
  65. if (!m) { console.log("unsupported image url for thumbnail fixer", element); return false; }
  66. if (150 / size >= window.devicePixelRatio) {
  67. element.style.backgroundImage = `url(${src_prefix}/c/150x150/img-master/${m[3]}${thumb_suffix})`;
  68. } else if (240 / size >= window.devicePixelRatio) {
  69. element.style.backgroundImage = `url(${src_prefix}/c/240x240/img-master/${m[3]}${thumb_suffix})`;
  70. } else if (360 / size >= window.devicePixelRatio) {
  71. element.style.backgroundImage = `url(${src_prefix}/c/360x360_70/img-master/${m[3]}${thumb_suffix})`;
  72. } else if (600 / size >= window.devicePixelRatio) {
  73. element.style.backgroundImage = `url(${src_prefix}/c/600x600/img-master/${m[3]}${thumb_suffix})`;
  74. } else { /* 1200 */
  75. element.style.backgroundImage = `url(${src_prefix}/img-master/${m[3]}${thumb_suffix})`;
  76. }
  77. return true;
  78. }
  79.  
  80. function findParentSize(node) {
  81. let size = 0;
  82. let e = node;
  83. while (!size) {
  84. if (e.attributes.width && e.attributes.height) {
  85. let width = +e.attributes.width.value;
  86. let height = +e.attributes.height.value;
  87. if (width && width > size) { size = width; }
  88. if (height && height > size) { size = height; }
  89. return size;
  90. }
  91. if (!e.parentElement) {
  92. console.log("Couldn't find a parent node with size set for", node);
  93. return size;
  94. }
  95. e = e.parentElement;
  96. }
  97. }
  98.  
  99. function handleImg(node) {
  100. if (node.dataset.kepstinThumbnail) { return; }
  101.  
  102. if (!node.src.startsWith(src_prefix)) { node.dataset.kepstinThumbnail = 'skip'; return; }
  103.  
  104. let m = node.src.match(src_regexp);
  105. if (!m) { node.dataset.kepstinThumbnail = 'bad'; return; }
  106.  
  107. let size = findParentSize(node);
  108. imgSrcset(node, size, m[3]);
  109. node.style.objectFit = 'contain';
  110.  
  111. node.dataset.kepstinThumbnail = 'ok';
  112. }
  113.  
  114. function handleLayoutThumbnail(node) {
  115. if (node.dataset.kepstinThumbnail) { return; }
  116.  
  117. if (/transparent.gif$/.test(node.src)) { return; }
  118.  
  119. let m = node.src.match(src_regexp);
  120. if (!m) { node.dataset.kepstinThumbnail = 'bad'; return; }
  121.  
  122. let size = Math.max(m[1], m[2]);
  123. if (!size) { size = 1200 };
  124. imgSrcset(node, size, m[3]);
  125. node.width = node.style.width = m[1];
  126. node.height = node.style.height = m[2];
  127. node.style.objectFit = 'contain';
  128.  
  129. node.dataset.kepstinThumbnail = 'ok';
  130. }
  131.  
  132. function onetimeThumbnails(parentNode) {
  133. for (let node of parentNode.querySelectorAll('img')) {
  134. if (node.parentElement.classList.contains('_layout-thumbnail')) {
  135. handleLayoutThumbnail(node);
  136. } else {
  137. handleImg(node);
  138. }
  139. }
  140. }
  141.  
  142. function handleDiscoveryDiv(node) {
  143. if (node.classList.contains('js-lazyload') || node.classList.contains('lazyloaded') || node.classList.contains('lazyloading')) { return; }
  144. if (node.dataset.kepstinThumbnail) { return; }
  145.  
  146. if (cssBackgroundImage(node)) {
  147. node.dataset.kepstinThumbnail = 'ok';
  148. }
  149. }
  150.  
  151. function mutationObserverCallback(mutationList, observer) {
  152. for (let mutation of mutationList) {
  153. switch (mutation.type) {
  154. case 'childList':
  155. for (let node of mutation.addedNodes) {
  156. if (node.nodeName == 'IMG') {
  157. handleImg(node);
  158. } else if (node.nodeName == 'DIV') {
  159. if (node.parentElement && node.parentElement.classList.contains('gtm-illust-recommend-thumbnail-link')) {
  160. handleDiscoveryDiv(node);
  161. } else {
  162. onetimeThumbnails(node);
  163. }
  164. }
  165. }
  166. break;
  167. case 'attributes':
  168. if (mutation.target.nodeName == 'DIV') {
  169. if (mutation.target.parentElement.classList.contains('gtm-illust-recommend-thumbnail-link')) {
  170. handleDiscoveryDiv(mutation.target);
  171. }
  172. } else if (mutation.target.nodeName == 'IMG') {
  173. if (mutation.target.parentElement.classList.contains('_layout-thumbnail')) {
  174. handleLayoutThumbnail(mutation.target);
  175. }
  176. }
  177. break;
  178. }
  179. }
  180. }
  181.  
  182. if (!window.kepstinThumbnailObserver) {
  183. onetimeThumbnails(document.firstElementChild);
  184. window.kepstinThumbnailObserver = new MutationObserver(mutationObserverCallback);
  185. window.kepstinThumbnailObserver.observe(document.firstElementChild, { childList: true, subtree: true, attributes: true, attributeFilter: [ 'class', 'src' ] });
  186. }
  187. })();