Save Image As...

Allows you to save an image in different formats.

  1. // ==UserScript==
  2. // @name Save Image As...
  3. // @description Allows you to save an image in different formats.
  4. // @namespace Meica05GOD
  5. // @version 2.4
  6. // @license MIT
  7. // @author GPT 4o
  8. // @match *://*/*
  9. // @grant GM_download
  10. // @grant GM_xmlhttpRequest
  11. // @run-at document-end
  12. // ==/UserScript==
  13.  
  14. (function() {
  15. 'use strict';
  16.  
  17. const lang = (navigator.language || 'en').toLowerCase();
  18. const isEs = lang.startsWith('es');
  19. const L = {
  20. menu: {
  21. png: isEs ? 'Guardar como PNG' : 'Save as PNG',
  22. jpg: isEs ? 'Guardar como JPG' : 'Save as JPG',
  23. webp: isEs ? 'Guardar como WEBP' : 'Save as WEBP',
  24. },
  25. alert: {
  26. httpError: status => isEs
  27. ? `Error HTTP ${status} al descargar la imagen.`
  28. : `HTTP error ${status} downloading image.`,
  29. networkError: isEs
  30. ? 'Error de red al descargar la imagen.'
  31. : 'Network error downloading image.',
  32. convertError: ext => isEs
  33. ? `No se pudo convertir la imagen a .${ext}`
  34. : `Could not convert image to .${ext}`,
  35. loadError: isEs
  36. ? 'Error cargando la imagen para conversión.'
  37. : 'Error loading image for conversion.',
  38. }
  39. };
  40.  
  41. const menu = document.createElement('div');
  42. Object.assign(menu.style, {
  43. position: 'fixed',
  44. background: '#fff',
  45. border: '1px solid #ccc',
  46. boxShadow: '2px 2px 5px rgba(0,0,0,0.2)',
  47. zIndex: 2147483647,
  48. padding: '5px 0',
  49. display: 'none',
  50. borderRadius: '4px',
  51. minWidth: '150px',
  52. fontFamily: 'sans-serif',
  53. fontSize: '14px'
  54. });
  55. document.body.appendChild(menu);
  56.  
  57. const formats = [
  58. { key: 'png', mime: 'image/png', ext: 'png' },
  59. { key: 'jpg', mime: 'image/jpeg', ext: 'jpg' },
  60. { key: 'webp', mime: 'image/webp', ext: 'webp' }
  61. ];
  62. formats.forEach(({ key, mime, ext }) => {
  63. const item = document.createElement('div');
  64. item.textContent = L.menu[key];
  65. Object.assign(item.style, {
  66. padding: '6px 12px',
  67. cursor: 'pointer',
  68. color: '#000',
  69. opacity: '1',
  70. pointerEvents: 'auto',
  71. });
  72. item.addEventListener('click', e => {
  73. e.stopPropagation();
  74. if (currentImgUrl) downloadAs(currentImgUrl, mime, ext);
  75. hideMenu();
  76. });
  77. menu.appendChild(item);
  78. });
  79.  
  80. let currentImgUrl = null;
  81.  
  82. document.addEventListener('contextmenu', function(e) {
  83. const target = e.target;
  84. let imgUrl = null;
  85. const imgEl = target.closest('img');
  86. if (imgEl && imgEl.src) {
  87. imgUrl = imgEl.src;
  88. } else {
  89. const bg = getComputedStyle(target).getPropertyValue('background-image');
  90. const m = bg.match(/url\(["']?(.*?)["']?\)/);
  91. if (m) imgUrl = m[1];
  92. }
  93.  
  94. if (e.shiftKey && imgUrl) {
  95. e.preventDefault();
  96. e.stopPropagation();
  97. e.stopImmediatePropagation();
  98.  
  99. currentImgUrl = imgUrl;
  100. showMenu(e.clientX, e.clientY);
  101. } else {
  102. hideMenu();
  103. }
  104. }, true);
  105.  
  106. document.addEventListener('click', e => {
  107. if (!menu.contains(e.target)) hideMenu();
  108. }, true);
  109.  
  110. function showMenu(x, y) {
  111. const mw = menu.offsetWidth, mh = menu.offsetHeight;
  112. const ww = window.innerWidth, wh = window.innerHeight;
  113. if (x + mw > ww) x = ww - mw - 5;
  114. if (y + mh > wh) y = wh - mh - 5;
  115. if (x < 0) x = 5;
  116. if (y < 0) y = 5;
  117.  
  118. menu.style.left = x + 'px';
  119. menu.style.top = y + 'px';
  120. menu.style.display = 'block';
  121. }
  122.  
  123. function hideMenu() {
  124. menu.style.display = 'none';
  125. currentImgUrl = null;
  126. }
  127.  
  128. function downloadAs(url, mimeType, ext) {
  129. GM_xmlhttpRequest({
  130. method: 'GET',
  131. url: url,
  132. responseType: 'blob',
  133. onload(resp) {
  134. if (resp.status >= 200 && resp.status < 300) {
  135. processBlob(resp.response, mimeType, ext);
  136. } else {
  137. alert(L.alert.httpError(resp.status));
  138. }
  139. },
  140. onerror() {
  141. alert(L.alert.networkError);
  142. }
  143. });
  144. }
  145.  
  146. function processBlob(blob, mimeType, ext) {
  147. const oUrl = URL.createObjectURL(blob);
  148. const img = new Image();
  149. img.onload = () => {
  150. const c = document.createElement('canvas');
  151. c.width = img.width; c.height = img.height;
  152. c.getContext('2d').drawImage(img, 0, 0);
  153. URL.revokeObjectURL(oUrl);
  154. c.toBlob(cb => {
  155. if (!cb) {
  156. alert(L.alert.convertError(ext));
  157. return;
  158. }
  159. const dUrl = URL.createObjectURL(cb);
  160. GM_download({ url: dUrl, name: `image_${Date.now()}.${ext}`, saveAs: true });
  161. setTimeout(() => URL.revokeObjectURL(dUrl), 10000);
  162. }, mimeType);
  163. };
  164. img.onerror = () => {
  165. URL.revokeObjectURL(oUrl);
  166. alert(L.alert.loadError);
  167. };
  168. img.src = oUrl;
  169. }
  170.  
  171. })();