ctrl+单击图片自动下载超级增强版

在图片对象上面:特别优化了Chrome浏览器下的点击拦截,不会点击后图片放大。1、按ctrl单击图片自动下载(默认转换为JPG格式);2、按alt 单击图片会弹窗选择命名方式(无需刷新网页即可生效),文件名可以根据当前日期时间time、网页域名domain、网页标题title,或用户自定义进行命名。

// ==UserScript==
// @name         ctrl+单击图片自动下载超级增强版
// @namespace    https://www.uiwow.com/
// @version      1.1
// @description  在图片对象上面:特别优化了Chrome浏览器下的点击拦截,不会点击后图片放大。1、按ctrl单击图片自动下载(默认转换为JPG格式);2、按alt 单击图片会弹窗选择命名方式(无需刷新网页即可生效),文件名可以根据当前日期时间time、网页域名domain、网页标题title,或用户自定义进行命名。
// @author       Techwb.cn
// @match        *://*/*
// @grant        GM_download
// @grant        GM_setValue
// @grant        GM_getValue
// @license      MIT
// ==/UserScript==

(function() {
  'use strict';
  var clickedImage = null;
  var nameOption = GM_getValue('nameOption', 'time'); //默认命名方式time
  var customName = GM_getValue('customName', '');
  var isDownloading = false;
  
  // 检测Chrome浏览器
  var isChrome = /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor);
  
  // 关键改进:使用多个事件阶段和类型进行拦截
  document.addEventListener('mousedown', handleMouseDown, true);
  document.addEventListener('click', handleClick, true);
  document.addEventListener('mouseup', handleMouseUp, true);
  
  // 拦截右键菜单,防止图片被打开
  if (isChrome) {
    document.addEventListener('contextmenu', function(e) {
      if (isDownloading && e.target.tagName === 'IMG') {
        e.preventDefault();
        e.stopImmediatePropagation();
      }
    }, true);
  }

  function handleMouseDown(event) {
    var target = event.target;
    
    if (isImageElement(target) && event.button === 0) {
      clickedImage = target;
      
      if (event.ctrlKey) {
        // 标记正在下载
        isDownloading = true;
        
        // 彻底阻止事件传播
        event.stopImmediatePropagation();
        event.preventDefault();
        
        // 特殊处理Chrome浏览器
        if (isChrome) {
          // 临时修改图片属性,防止浏览器打开
          var originalSrc = clickedImage.src;
          clickedImage.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==';
          
          // 延迟执行下载,确保属性修改生效
          setTimeout(function() {
            handleDownload(originalSrc);
            
            // 恢复图片源(可选)
            setTimeout(function() {
              clickedImage.src = originalSrc;
              isDownloading = false;
            }, 100);
          }, 0);
        } else {
          handleDownload();
        }
      } else if (event.altKey) {
        event.preventDefault();
        handleNaming();
      }
    }
  }

  function handleClick(event) {
    // 确保Ctrl+点击事件不会触发其他点击处理
    if (isDownloading && event.button === 0) {
      event.stopImmediatePropagation();
      event.preventDefault();
    }
  }

  function handleMouseUp(event) {
    // 重置下载状态
    if (isDownloading && event.button === 0) {
      setTimeout(function() {
        isDownloading = false;
      }, 500);
    }
  }

  // 增强版:判断元素是否是图片或图片容器
  function isImageElement(element) {
    if (element.tagName === 'IMG') return true;
    
    // 检查是否是常见的图片查看器容器
    if (element.classList.contains('fancybox-image') || 
        element.classList.contains('magnifier-image') || 
        element.classList.contains('zoom-img') ||
        element.classList.contains('photo-gallery-image')) {
      return true;
    }
    
    // 检查内部是否包含图片
    var img = element.querySelector('img');
    if (img && element.getBoundingClientRect().width === img.getBoundingClientRect().width) {
      return true;
    }
    
    return false;
  }

  function handleDownload(forcedSrc) {
    if (!clickedImage) return;
    
    // 使用强制提供的src(如果有)
    var imgSrc = forcedSrc || getRealImageUrl(clickedImage);
    if (!imgSrc) {
      console.log('无法获取图片URL');
      return;
    }
    
    // 尝试使用GM_download,失败则回退到原生方法
    try {
      // 先获取原始文件名
      var fileName = getFileName(imgSrc);
      
      // 如果是GIF,进一步检查是否为动画
      if (fileName.toLowerCase().endsWith('.gif')) {
        checkIfAnimatedGif(imgSrc).then(isAnimated => {
          // 如果不是动画且用户偏好其他格式,可以考虑转换
          // 这里保持原有行为,只确保正确的扩展名
          downloadWithName(imgSrc, fileName);
        }).catch(err => {
          console.log('GIF动画检测失败,使用默认文件名:', err);
          downloadWithName(imgSrc, fileName);
        });
      } else {
        // 非GIF图片直接下载
        downloadWithName(imgSrc, fileName);
      }
    } catch (e) {
      console.log('下载出错,回退到原生方法:', e);
      fallbackDownload(imgSrc, getFileName(imgSrc));
    }
  }

  function downloadWithName(imgSrc, fileName) {
    if (typeof GM_download !== 'undefined') {
      GM_download({
        url: imgSrc,
        name: fileName,
        onerror: function(err) {
          console.log('GM_download失败,回退到原生方法:', err);
          fallbackDownload(imgSrc, fileName);
        }
      });
    } else {
      fallbackDownload(imgSrc, fileName);
    }
  }

  // 原生下载方法,增强了对不同URL类型的处理
  function fallbackDownload(imgSrc, fileName) {
    // 处理dataURL
    if (imgSrc.startsWith('data:')) {
      var a = document.createElement('a');
      a.href = imgSrc;
      a.download = fileName;
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      return;
    }
    
    // 处理普通URL
    fetch(imgSrc, { mode: 'cors' })
      .then(function(response) {
        if (!response.ok) throw new Error('网络响应错误: ' + response.status);
        return response.blob();
      })
      .then(function(blob) {
        var url = URL.createObjectURL(blob);
        var a = document.createElement('a');
        a.href = url;
        a.download = fileName;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);
      })
      .catch(function(error) {
        console.log('下载失败:', error);
        alert('下载失败: ' + error.message);
      });
  }

  // 增强版:获取真实图片URL,处理各种情况
  function getRealImageUrl(imgElement) {
    // 优先使用data-src等可能的真实图片源
    var imgSrc = imgElement.dataset.src || 
                 imgElement.dataset.original || 
                 imgElement.dataset.lazySrc || 
                 imgElement.dataset.image || 
                 imgElement.src;
    
    // 处理图片代理URL
    if (imgSrc.includes('proxy?url=')) {
      imgSrc = decodeURIComponent(imgSrc.split('proxy?url=')[1]);
    }
    
    // 确保URL有效
    if (!imgSrc || imgSrc.startsWith('data:')) {
      return imgSrc;
    }
    
    // 添加协议头(如果缺失)
    if (imgSrc.startsWith('//')) {
      imgSrc = window.location.protocol + imgSrc;
    }
    
    return imgSrc;
  }

  // 关键改进:智能识别图片格式,保持GIF不变
  function getFileName(imgUrl) {
    var fileName = '';
    if (nameOption === 'time') {
      fileName = '图片_' + getDate();
    } else if (nameOption === 'domain') {
      fileName = getDomain();
    } else if (nameOption === 'title') {
      fileName = getTitle();
    } else {
      fileName = customName;
    }
    
    // 从URL中提取文件扩展名
    var ext = getFileExtension(imgUrl);
    
    // 过滤非法文件名字符
    fileName = fileName.replace(/[\\/:*?"<>|]/g, '_');
    
    // 添加扩展名(如果有)
    return ext ? fileName + '.' + ext : fileName + '.jpg';
  }

  // 关键改进:获取并保留原始图片格式
  function getFileExtension(url) {
    if (!url) return '';
    
    // 尝试从URL中提取扩展名
    var ext = url.split('?')[0].split('.').pop().toLowerCase();
    
    // 检查是否是有效的图片扩展名
    var validExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg'];
    
    if (validExtensions.includes(ext)) {
      return ext;
    }
    
    // 如果无法确定,尝试从Content-Type判断(针对dataURL)
    if (url.startsWith('data:')) {
      var matches = url.match(/data:image\/([^;]+);/);
      if (matches && matches[1]) {
        return matches[1].toLowerCase();
      }
    }
    
    // 默认返回jpg(保持原有行为)
    return '';
  }

  // 新增:检测GIF是否为动画
  function checkIfAnimatedGif(url) {
    return new Promise((resolve, reject) => {
      // 对于dataURL直接处理
      if (url.startsWith('data:')) {
        checkGifData(url).then(resolve).catch(reject);
        return;
      }
      
      // 对于普通URL,先获取二进制数据
      fetch(url)
        .then(response => response.arrayBuffer())
        .then(buffer => {
          checkGifBuffer(buffer).then(resolve).catch(reject);
        })
        .catch(reject);
    });
  }

  // 检测GIF数据是否包含多个帧
  function checkGifBuffer(buffer) {
    return new Promise((resolve, reject) => {
      try {
        // 检查GIF签名 (GIF87a 或 GIF89a)
        const signature = new TextDecoder().decode(buffer.slice(0, 6));
        if (signature !== 'GIF87a' && signature !== 'GIF89a') {
          resolve(false);
          return;
        }
        
        // 查找动画帧标记 (0x2C)
        const view = new DataView(buffer);
        let offset = 13; // 跳过头部
        
        // 查找图像分隔符 (0x2C)
        let frameCount = 0;
        while (offset < view.byteLength) {
          const code = view.getUint8(offset);
          
          if (code === 0x2C) { // 图像分隔符
            frameCount++;
            if (frameCount > 1) {
              resolve(true); // 发现多个帧,是动画
              return;
            }
            // 跳过图像数据
            offset += 9; // 跳到图像宽度字段
            offset += 2; // 跳过宽度
            offset += 2; // 跳过高度
            offset += 1; // 跳过标志位
            
            // 跳过局部颜色表
            const flags = view.getUint8(offset - 1);
            if (flags & 0x80) { // 有局部颜色表
              const colorTableSize = (flags & 0x07) + 1;
              offset += 3 * Math.pow(2, colorTableSize);
            }
            
            // 跳过图像数据
            while (offset < view.byteLength) {
              const blockSize = view.getUint8(offset);
              if (blockSize === 0) break;
              offset += blockSize + 1;
            }
            offset++; // 跳过块终止符
          } else if (code === 0x21) { // 扩展块
            offset += 1; // 跳过扩展标签
            // 跳过扩展块
            while (offset < view.byteLength) {
              const blockSize = view.getUint8(offset);
              if (blockSize === 0) break;
              offset += blockSize + 1;
            }
            offset++; // 跳过块终止符
          } else {
            break; // 不是有效的GIF格式
          }
        }
        
        resolve(frameCount > 1); // 如果只有一帧,不是动画
      } catch (error) {
        console.error('GIF解析错误:', error);
        resolve(false); // 解析失败,保守地认为不是动画
      }
    });
  }

  // 处理dataURL格式的GIF
  function checkGifData(dataUrl) {
    return new Promise((resolve, reject) => {
      try {
        // 提取base64部分
        const base64 = dataUrl.split(',')[1];
        if (!base64) {
          resolve(false);
          return;
        }
        
        // 转换为ArrayBuffer
        const binary = atob(base64);
        const buffer = new ArrayBuffer(binary.length);
        const view = new Uint8Array(buffer);
        
        for (let i = 0; i < binary.length; i++) {
          view[i] = binary.charCodeAt(i);
        }
        
        // 检查GIF数据
        checkGifBuffer(buffer).then(resolve).catch(reject);
      } catch (error) {
        console.error('GIF dataURL解析错误:', error);
        resolve(false);
      }
    });
  }

  // 以下是原脚本的其他功能,保持不变...
  function handleNaming() {
    var dateTime = getDate();
    var domainName = getDomain();
    var titleName = getTitle();
    var defaultName = dateTime + '' + titleName + '' + domainName + '.jpg';

    var optionsHtml = '<option value="time" ' + (nameOption === 'time' ? 'selected' : '') + '>时间命名方式</option>' +
                      '<option value="domain" ' + (nameOption === 'domain' ? 'selected' : '') + '>域名命名方式</option>' +
                      '<option value="title" ' + (nameOption === 'title' ? 'selected' : '') + '>标题命名方式</option>' +
                      '<option value="custom" ' + (nameOption === 'custom' ? 'selected' : '') + '>自定义命名方式</option>';

    var html = '<div><label for="nameOption">命名方式:</label><select id="nameOption">' + optionsHtml + '</select></div>';
    var customNameHtml = '<div style="display:none;">' + // 初始隐藏
               '<label for="customName">请输入文件名:</label>' +
               '<input type="text" id="customName" value="' + customName + '">' +
               '</div>';
    var fileNameHtml = '<div><label for="fileName">文件名:</label><input type="text" id="fileName" value="' + defaultName + '" readonly></div>';

    var dialog = document.createElement('div');
    dialog.innerHTML = html + customNameHtml + fileNameHtml;
    dialog.style.position = 'fixed';
    dialog.style.top = '50%';
    dialog.style.left = '50%';
    dialog.style.transform = 'translate(-50%, -50%)';
    dialog.style.backgroundColor = '#fff';
    dialog.style.padding = '20px';
    dialog.style.borderRadius = '5px';
    dialog.style.boxShadow = '0px 0px 10px rgba(0, 0, 0, 0.5)';
    dialog.style.zIndex = '9999';
    document.body.appendChild(dialog);

    var select = dialog.querySelector('#nameOption');
    var customNameInput = dialog.querySelector('#customName');
    var fileNameInput = dialog.querySelector('#fileName');
    var closeButton = document.createElement('button');
    closeButton.innerText = '关闭';
    closeButton.style.position = 'absolute';
    closeButton.style.top = '2px';
    closeButton.style.right = '2px';
    closeButton.style.padding = '3px 5px';
    closeButton.style.backgroundColor = '#e74c3c';
    closeButton.style.border = 'none';
    closeButton.style.color = '#fff';
    closeButton.style.borderRadius = '3px';
    closeButton.style.cursor = 'pointer';
    closeButton.addEventListener('click', function() {
      document.body.removeChild(dialog);
    });
    dialog.appendChild(closeButton);

    select.addEventListener('change', function() {
      nameOption = select.value;
      updateFileName();
      GM_setValue('nameOption', nameOption);
      if (nameOption === 'custom') {
        customNameInput.parentNode.style.display = 'block'; // 显示
      } else {
        customNameInput.parentNode.style.display = 'none'; // 隐藏
        GM_setValue('customName', '');
      }
    });

    customNameInput.addEventListener('input', function() {
      customName = customNameInput.value;
      updateFileName();
      GM_setValue('customName', customName);
    });

    function updateFileName() {
      var fileName = '';
      if (nameOption === 'time') {
        fileName = '图片_' + getDate();
      } else if (nameOption === 'domain') {
        fileName = getDomain();
      } else if (nameOption === 'title') {
        fileName = getTitle();
      } else {
        fileName = customName;
      }
      
      // 这里默认显示为jpg,实际下载时会根据图片类型自动调整
      fileNameInput.value = fileName + '.jpg';
    }

    updateFileName();
  }

  function getDate() {
    var now = new Date();
    return now.getFullYear() + '-' + (now.getMonth() + 1).toString().padStart(2, '0') + '-' + now.getDate().toString().padStart(2, '0') + '_' + now.getHours().toString().padStart(2, '0') + now.getMinutes().toString().padStart(2, '0') + now.getSeconds().toString().padStart(2, '0');
  }

  function getDomain() {
    var url = window.location.href;
    var domain = url.split('/')[2];
    domain = domain.replace(/www\./, '');
    return domain;
  }

  function getTitle() {
    var title = document.title;
    return title.substring(0, 10).replace(/[\\/:*?"<>|]/g, '_');
  }
})();