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