Search By Image

Search By Image | 以图搜图

目前为 2015-04-06 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name Search By Image
  3. // @version 1.4.7
  4. // @description Search By Image | 以图搜图
  5. // @match <all_urls>
  6. // @include *
  7. // @author 864907600cc
  8. // @icon http://1.gravatar.com/avatar/147834caf9ccb0a66b2505c753747867
  9. // @run-at document-start
  10. // @grant GM_getValue
  11. // @grant GM_setValue
  12. // @grant GM_openInTab
  13. // @grant GM_registerMenuCommand
  14. // @namespace http://ext.ccloli.com
  15. // ==/UserScript==
  16.  
  17.  
  18. // 本脚本基于 GPLv3 协议开源 http://www.gnu.org/licenses/gpl.html‎
  19. // (c) 86497600cc. Some Rights Reserved.
  20. // Default setting: Press Ctrl and click right key on a image to search.
  21.  
  22. 'use strict';
  23. var default_setting = {
  24. "site_list": {
  25. "Google": "https://www.google.com/searchbyimage?image_url={%s}",
  26. "Baidu ShiTu": "http://stu.baidu.com/i?ct=1&tn=baiduimage&objurl={%s}",
  27. "Baidu Image": "http://image.baidu.com/i?rainbow=1&ct=1&tn=shituresultpc&objurl={%s}",
  28. "Bing": "http://cn.bing.com/images/searchbyimage?FORM=IRSBIQ&cbir=sbi&imgurl={%s}",
  29. "TinEye": "http://www.tineye.com/search?url={%s}",
  30. //"Cydral": "http://www.cydral.com/#url={%s}",
  31. "Yandex": "http://yandex.ru/images/search?rpt=imageview&img_url={%s}", // change "Яндекс (Yandex)" to "Yandex"
  32. "Sogou": "http://pic.sogou.com/ris?query={%s}",
  33. "360 ShiTu": "http://st.so.com/stu?imgurl={%s}",
  34. "SauceNAO": "http://saucenao.com/search.php?db=999&url={%s}",
  35. "IQDB": "http://iqdb.org/?url={%s}",
  36. "3D IQDB": "http://3d.iqdb.org/?url={%s}"
  37. },
  38. "site_option": ["Google", "Baidu ShiTu", "Baidu Image", "Bing", "TinEye", "Yandex", "Sogou", "360 ShiTu", "SauceNAO", "IQDB", "3D IQDB"],
  39. "hot_key": "ctrlKey"
  40. };
  41.  
  42. var server_url = "//sbi.ccloli.com/img/upload.php";
  43. // 在此处直接输入完整的上传页面的地址(Firefox 请尽量选择支持 https 的服务器)
  44. // 地址前使用"//"表示按照当前页面设定决定是否使用 https
  45. // 地址前使用"http://"表示强制使用 http
  46. // 地址前使用"https://"表示强制使用 https(需确认服务器支持 ssl)
  47. // 如果需要自己架设上传服务器的话请访问 GitHub 项目页(https://github.com/ccloli/Search-By-Image)获取服务端
  48. // 其他可用的上传服务器如下:
  49. // Heroku://search-by-image.herokuapp.com/img/upload.php (支持 https)
  50. // BeGet: http://fh13121a.bget.ru/img/upload.php (不支持 https)
  51. // OpenShift://searchbyimage-864907600cc.rhcloud.com/img/upload.php (支持 https)
  52. // DigitalOcean VPS://sbi.ccloli.com/img/upload.php (支持 https,thanks to Retaker)
  53. // 注意,部分服务器可能仅支持 http 协议,若您选择了这些服务器,请务必注明 "http://",且若您使用的是 Firefox 浏览器,在 https 页面下将不能上传文件搜索搜索(除非设置 security.mixed_content.block_active_content 为 false)
  54.  
  55. var search_panel = null;
  56. var setting = GM_getValue('setting') ? JSON.parse(GM_getValue('setting')) : default_setting;
  57. var disable_contextmenu = false;
  58. var img_src = null;
  59. var data_version = GM_getValue('version', 0);
  60. var last_update = GM_getValue('timestamp', 0);
  61. var xhr = new XMLHttpRequest();
  62. var reader = new FileReader();
  63. reader.onload = function(file) {
  64. upload_file(this.result);
  65. };
  66.  
  67. function set_setting(data) {
  68. GM_setValue('setting', JSON.stringify(data));
  69. GM_setValue('timestamp', new Date().getTime());
  70. }
  71.  
  72. function create_panel() {
  73. search_panel = document.createElement('div');
  74. search_panel.style.cssText = 'width: 198px; font-size: 14px; text-align: center; position: absolute; color: #000; z-index: 9999999999; box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.5); border: 1px solid #CCC; background: rgba(255, 255, 255, 0.9); border-top-right-radius: 2px; border-bottom-left-radius: 2px; font-family: "Arial"; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none;';
  75. document.body.appendChild(search_panel);
  76. var search_top = document.createElement('div');
  77. search_top.style.cssText = 'height: 24px; line-height: 24px; font-size: 12px; overflow: hidden; margin: 0 auto; padding: 0 5px;';
  78. search_top.className = 'image-search-top';
  79. search_top.innerHTML = '<div class="search_top_url" style="overflow: hidden; white-space: nowrap; text-overflow: ellipsis; width: 100%; height: 24px;"></div><div class="search_top_file" style="width: 100%; height: 24px; line-height: 24px;" draggable="true"><label for="image-search-file">上传图片并搜索</label><input type="file" id="image-search-file" accept="image/*" style="width: 0px; height: 0px; max-height: 0px; max-width: 0px; margin: 0; padding: 0;"></div><div class="search_top_progress"><progress style="width: 100%; height: 16px; vertical-align: middle; margin: 4px 0;" max="1"></progerss></div><style>.image-search-item{color: #000000; transition: all 0.2s linear; -webkit-transition: all 0.1s linear;}.image-search-item:hover{background: #eeeeee;}</style>';
  80. search_panel.appendChild(search_top);
  81. var search_item = document.createElement('div');
  82. search_item.style.cssText = 'width: 100%; height: 24px; line-height: 24px; cursor: pointer;';
  83. search_item.className = 'image-search-item';
  84. for (var i in setting.site_list) {
  85. var search_item_child = search_item.cloneNode(true);
  86. search_item_child.textContent = i;
  87. search_item_child.setAttribute('search-option', i);
  88. search_panel.appendChild(search_item_child);
  89. }
  90. search_item.textContent = 'All';
  91. search_item.setAttribute('search-option', 'all');
  92. search_panel.appendChild(search_item);
  93. var search_item_setting = search_item.cloneNode(true);
  94. search_item_setting.textContent = 'Setting';
  95. search_item_setting.setAttribute('search-option', 'setting');
  96. search_panel.appendChild(search_item_setting);
  97. search_top.getElementsByTagName('input')[0].onchange = function() {
  98. reader.readAsDataURL(this.files[0]);
  99. };
  100. search_panel.ondragenter = function(event) {
  101. event.preventDefault();
  102. search_top.getElementsByTagName('label')[0].textContent = '拖拽文件至此';
  103. };
  104. search_panel.ondragleave = function(event) {
  105. event.preventDefault();
  106. search_top.getElementsByTagName('label')[0].textContent = '上传图片并搜索';
  107. };
  108. search_panel.ondragover = function(event) {
  109. search_top.getElementsByTagName('label')[0].textContent = '拖拽文件至此';
  110. event.preventDefault();
  111. };
  112. search_panel.ondrop = function(event) {
  113. event.stopPropagation();
  114. event.preventDefault();
  115. var files = event.target.files || event.dataTransfer.files;
  116. if (files[files.length - 1].type.indexOf('image') >= 0) reader.readAsDataURL(files[files.length - 1]);
  117. };
  118. search_top.getElementsByTagName('progress')[0].onclick = function() {
  119. if (xhr.readyState != 0 && confirm('确认终止上传文件吗?') == true) {
  120. xhr.abort();
  121. search_panel.getElementsByClassName('search_top_url')[0].style.marginTop = '-24px';
  122. }
  123. };
  124. if (navigator.userAgent.indexOf('Firefox') >= 0) {
  125. var paste_node_firefox = document.createElement('div');
  126. paste_node_firefox.setAttribute('contenteditable', 'true');
  127. paste_node_firefox.className = 'image-search-paste-node-firefox';
  128. paste_node_firefox.style.cssText = 'width: 0!important; height: 0!important; position: absolute; overflow: hidden;';
  129. paste_node_firefox.addEventListener('paste', function(event) {
  130. setTimeout(function() {
  131. var _images = paste_node_firefox.getElementsByTagName('img');
  132. if (_images.length > 0) {
  133. var _img_src = _images[_images.length - 1].src;
  134. if (_img_src.match(/^data: .*?; base64, /)) upload_file(_img_src);
  135. }
  136. }, 500);
  137. }, false);
  138. search_top.appendChild(paste_node_firefox);
  139. }
  140. }
  141.  
  142. function call_setting() {
  143. var setting_panel = document.createElement('div');
  144. setting_panel.style.cssText = 'width: 520px; font-size: 14px; position: fixed; color: #000; z-index: 9999999999; box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.5); border: 1px solid #CCC; background: rgba(255, 255, 255, 0.9); border-top-right-radius: 2px; border-bottom-left-radius: 2px; padding: 10px; left: 0; right: 0; top: 0; bottom: 0; margin: auto; font-family: "Arial"; height: 400px; max-height: 90%; overflow: auto; text-align: center; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none;';
  145. document.body.appendChild(setting_panel);
  146. var setting_header = document.createElement('div');
  147. setting_header.style.cssText = 'width: 100%; height: 32px; line-height: 32px; font-size: 18px; line-height: 32px;';
  148. setting_header.className = 'image-search-setting-header';
  149. setting_header.textContent = 'Search By Image Setting';
  150. setting_panel.appendChild(setting_header);
  151. var setting_item = document.createElement('div');
  152. setting_item.style.cssText = 'width: 100%; height: 24px; line-height: 24px; margin: 1px 0;';
  153. setting_item.className = 'image-search-setting-title';
  154. setting_item.innerHTML = '<div style="text-align: center; display: inline-block; width: 30px;">多搜</div><div style="width: 100px; text-align: center; display: inline-block;">名称</div><div style="width: 350px; text-align: center; display: inline-block;">地址(图片地址以 {%s} 代替)</div><div style="width: 20px; display: inline-block;"></div>';
  155. setting_panel.appendChild(setting_item);
  156. for (var i in setting.site_list) {
  157. var setting_item_child = setting_item.cloneNode(true);
  158. setting_item_child.className = 'image-search-setting-item';
  159. setting_item_child.innerHTML = '<div style="text-align: center; display: inline-block; width: 30px; vertical-align: middle;"><input type="checkbox"' + (setting.site_option.join('\n').indexOf(i) >= 0 ? ' checked="checked"' : '') + '></div><div style="width: 100px; text-align: center; display: inline-block;"><input style="width: 90px;" type="text" value="' + i + '"></div><div style="width: 350px; text-align: center; display: inline-block;"><input style="width: 340px;" type="text" value="' + setting.site_list[i] + '"></div><div style="text-align: center; display: inline-block; cursor: pointer; width: 20px;">×</div>';
  160. setting_panel.appendChild(setting_item_child);
  161. setting_item_child.getElementsByTagName('div')[3].onclick = function() {
  162. this.parentElement.outerHTML = '';
  163. };
  164. }
  165. var setting_footer = document.createElement('div');
  166. setting_footer.style.cssText = 'width: 100%; height: 32px; line-height: 32px; margin-top: 5px; text-align: right;';
  167. setting_footer.className = 'image-search-setting-footer';
  168. setting_panel.appendChild(setting_footer);
  169. var setting_hotkey = document.createElement('div');
  170. var setting_add = document.createElement('div');
  171. var setting_reset = document.createElement('div');
  172. var setting_save = document.createElement('div');
  173. var setting_cancel = document.createElement('div');
  174. setting_hotkey.style.cssText = 'height: 32px; display: inline-block; text-align: left; float: left;';
  175. setting_add.style.cssText = 'width: 90px; height: 32px; margin: 0 5px; background: #666; color: #FFF; display: inline-block; text-align: center; cursor: pointer;';
  176. setting_reset.style.cssText = 'width: 90px; height: 32px; background: #666; color: #FFF; display: inline-block; text-align: center; cursor: pointer;';
  177. setting_save.style.cssText = 'width: 90px; height: 32px; margin: 0 5px; background: #666; color: #FFF; display: inline-block; text-align: center; cursor: pointer;';
  178. setting_cancel.style.cssText = 'width: 90px; height: 32px; background: #666; color: #FFF; display: inline-block; text-align: center; cursor: pointer;';
  179. setting_add.textContent = 'Add Item';
  180. setting_reset.textContent = 'Reset';
  181. setting_save.textContent = 'Save';
  182. setting_cancel.textContent = 'Cancel';
  183. setting_hotkey.innerHTML = 'Hot Key <select><option value="ctrlKey"' + (setting.hot_key == 'ctrlKey' ? ' selected' : '') + '>Ctrl</option><option value="shiftKey"' + (setting.hot_key == 'shiftKey' ? ' selected' : '') + '>Shift</option><option value="altKey"' + (setting.hot_key == 'altKey' ? ' selected' : '') + '>Alt</option></select>';
  184. setting_footer.appendChild(setting_hotkey);
  185. setting_footer.appendChild(setting_add);
  186. setting_footer.appendChild(setting_reset);
  187. setting_footer.appendChild(setting_save);
  188. setting_footer.appendChild(setting_cancel);
  189. setting_add.onclick = function() {
  190. var setting_item_child = setting_item.cloneNode(true);
  191. setting_item_child.className = 'image-search-setting-item';
  192. setting_item_child.innerHTML = '<div style="text-align: center; display: inline-block; width: 30px; vertical-align: middle;"><input type="checkbox"></div><div style="width: 100px; text-align: center; display: inline-block;"><input style="width: 90px;" type="text"></div><div style="width: 350px; text-align: center; display: inline-block;"><input style="width: 340px;" type="text"></div><div style="text-align: center; display: inline-block; cursor: pointer; width: 20px;">×</div>';
  193. setting_panel.insertBefore(setting_item_child, setting_footer);
  194. setting_item_child.getElementsByTagName('div')[3].onclick = function() {
  195. this.parentElement.outerHTML = '';
  196. };
  197. setting_panel.scrollTop = setting_panel.scrollHeight;
  198. };
  199. setting_reset.onclick = function() {
  200. if (confirm('确定将所有设置初始化么?\n\n(初始化将清除所有所有设置,且不可逆)') == true) {
  201. setting = default_setting;
  202. set_setting(setting);
  203. setting_panel.outerHTML = '';
  204. if (search_panel != null) {
  205. search_panel.outerHTML = '';
  206. search_panel = null;
  207. }
  208. call_setting();
  209. }
  210. };
  211. setting_save.onclick = function() {
  212. var setting_items = document.getElementsByClassName('image-search-setting-item');
  213. var setting_data = {
  214. "site_list": {},
  215. "site_option": [],
  216. "hot_key": null
  217. };
  218. for (var i = 0; i < setting_items.length; i++) {
  219. if (setting_items[i].getElementsByTagName('input')[1].value != '') {
  220. if (setting_items[i].getElementsByTagName('input')[0].checked) setting_data.site_option.push(setting_items[i].getElementsByTagName('input')[1].value);
  221. setting_data.site_list[setting_items[i].getElementsByTagName('input')[1].value] = setting_items[i].getElementsByTagName('input')[2].value;
  222. }
  223. }
  224. setting_data.hot_key = setting_hotkey.getElementsByTagName('select')[0].value;
  225. console.log(setting_data);
  226. setting = setting_data;
  227. set_setting(setting);
  228. setting_panel.outerHTML = '';
  229. if (search_panel != null) {
  230. search_panel.outerHTML = '';
  231. search_panel = null;
  232. }
  233. };
  234. setting_cancel.onclick = function() {
  235. setting_panel.outerHTML = '';
  236. };
  237. }
  238.  
  239. function upload_file(data) {
  240. if (xhr.readyState != 0) xhr.abort();
  241. xhr.onreadystatechange = function() {
  242. if (xhr.readyState == 4) {
  243. if (xhr.status == 200) {
  244. img_src = xhr.responseText;
  245. search_panel.getElementsByClassName('search_top_url')[0].style.marginTop = '0px';
  246. search_panel.getElementsByClassName('search_top_url')[0].textContent = '上传完成!';
  247. }
  248. }
  249. };
  250. xhr.upload.onprogress = function(event) {
  251. search_panel.getElementsByTagName('progress')[0].value = event.loaded / event.total;
  252. };
  253. xhr.onerror = function() {
  254. alert('上传失败!');
  255. };
  256. var form = new FormData();
  257. xhr.open('POST', server_url);
  258. form.append('imgdata', data);
  259. xhr.send(form);
  260. search_panel.getElementsByClassName('search_top_url')[0].style.marginTop = '-48px';
  261. }
  262.  
  263. function get_clipboard(event) {
  264. var items = event.clipboardData.items;
  265. if (items[items.length - 1].type.indexOf('image') >= 0) reader.readAsDataURL(items[items.length - 1].getAsFile());
  266. }
  267.  
  268. function hide_panel() {
  269. img_src = null;
  270. //search_panel.style.display = 'none';
  271. search_panel.parentElement.removeChild(search_panel); // Remove search panel to fix the bug that it may be left in some WYSIWYG editor (eg. MDN WYSIWYG editor). See issue: http://tieba.baidu.com/p/3682475061
  272. document.removeEventListener('paste', get_clipboard, false);
  273. }
  274.  
  275. document.addEventListener('mousedown', function(event) { // In order to fix a bug on Chrome Tampermonkey
  276. //document.onmousedown=function(event){
  277. //console.log('Search Image >>\nevent.ctrlKey: '+event.ctrlKey+'\nevent.button: '+event.button+'\nevent.target.tagName: '+event.target.tagName+'\nevent.target.src: '+event.target.src+'\nevent.pageX: '+event.pageX+'\nevent.pageY: '+event.pageY+'\ndocument.documentElement.clientWidth: '+document.documentElement.clientWidth+'\ndocument.documentElement.clientHeight: '+document.documentElement.clientHeight+'\ndocument.documentElement.scrollWidth: '+document.documentElement.scrollWidth+'\ndocument.documentElement.scrollHeight: '+document.documentElement.scrollHeight+'\ndocument.documentElement.scrollLeft: '+document.documentElement.scrollLeft+'\ndocument.documentElement.scrollTop: '+document.documentElement.scrollTop);
  278. if (disable_contextmenu == true) {
  279. document.oncontextmenu = null;
  280. disable_contextmenu = false;
  281. }
  282. if (event[setting.hot_key] == true && event.button == 2) {
  283. if (search_panel == null) create_panel();
  284. else if (last_update != GM_getValue('timestamp', 0)) {
  285. last_update = GM_getValue('timestamp', 0)
  286. search_panel.outerHTML = '';
  287. setting = GM_getValue('setting') ? JSON.parse(GM_getValue('setting')) : default_setting;
  288. create_panel();
  289. }
  290. else document.body.appendChild(search_panel);//search_panel.style.display = 'block';
  291. search_panel.style.left = (document.documentElement.offsetWidth + (document.documentElement.scrollLeft || document.body.scrollLeft) - event.pageX >= 200 ? event.pageX : event.pageX >= 200 ? event.pageX - 200 : 0) + 'px';
  292. search_panel.style.top = (document.documentElement.scrollHeight - event.pageY >= search_panel.scrollHeight ? event.pageY : event.pageY >= search_panel.scrollHeight ? event.pageY - search_panel.scrollHeight : 0) + 'px';
  293. // Firefox doesn't support getComputedStyle(element).marginLeft/marginRight and it would return "0px" while the element's margin is "auto". See bugzila/381328.
  294. search_panel.style.marginLeft = '-' + (navigator.userAgent.indexOf('Firefox') < 0 ? getComputedStyle(document.body).marginLeft : (document.documentElement.offsetWidth - document.body.offsetWidth) / 2 + 'px');
  295. search_panel.style.marginTop = '-' + getComputedStyle(document.body).marginTop;
  296. disable_contextmenu = true;
  297. //for(var i=0; i<setting.site_option.length; i++)GM_openInTab(setting.site_list[setting.site_option[i]].replace(/\{%s\}/, encodeURIComponent(event.target.src)));
  298. document.oncontextmenu = function() {
  299. return false;
  300. };
  301. if (event.target.tagName.toLowerCase() == 'img' && event.target.src != null) {
  302. search_panel.getElementsByClassName('search_top_url')[0].style.marginTop = '0px';
  303. search_panel.getElementsByClassName('search_top_url')[0].textContent = event.target.src;
  304. if (event.target.src.match(/^data: .*?; base64, /) != null) upload_file(event.target.src);
  305. else img_src = event.target.src;
  306. } else {
  307. search_panel.getElementsByClassName('search_top_url')[0].style.marginTop = '-24px';
  308. if (navigator.userAgent.indexOf('Firefox') >= 0) {
  309. document.getElementsByClassName('image-search-paste-node-firefox')[0].innerHTML = '';
  310. document.getElementsByClassName('image-search-paste-node-firefox')[0].focus();
  311. } else document.addEventListener('paste', get_clipboard, false);
  312. }
  313. } else if (search_panel != null) {
  314. if (event.target.compareDocumentPosition(search_panel) == 10 || event.target.compareDocumentPosition(search_panel) == 0) {
  315. if (event.target.className == 'image-search-item' && event.button == 0) {
  316. switch (event.target.getAttribute('search-option')) {
  317. case 'all':
  318. if (img_src != null) {
  319. for (var i = setting.site_option.length - 1; i >= 0; i--) GM_openInTab(setting.site_list[setting.site_option[i]].replace(/\{%s\}/, encodeURIComponent(img_src)));
  320. hide_panel();
  321. }
  322. break;
  323. case 'setting':
  324. call_setting();
  325. hide_panel();
  326. break;
  327. default:
  328. if (img_src != null) {
  329. GM_openInTab(setting.site_list[event.target.getAttribute('search-option')].replace(/\{%s\}/, encodeURIComponent(img_src)));
  330. hide_panel();
  331. }
  332. }
  333. } else if (event.button != 0) hide_panel();
  334. } else hide_panel();
  335. }
  336. }, false);
  337.  
  338. var gm_callsetting = GM_registerMenuCommand('Search By Image Setting', call_setting);