Improve pixiv thumbnails

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

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

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