4chan Image Resizer

Automatically downscales images based on custom presets, image cropping and more. Requires 4chan X.

当前为 2021-03-28 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name 4chan Image Resizer
  3. // @namespace https://greasyfork.org/en/users/393416
  4. // @version 2.3
  5. // @description Automatically downscales images based on custom presets, image cropping and more. Requires 4chan X.
  6. // @author greenronia
  7. // @match *://boards.4chan.org/*
  8. // @match *://boards.4channel.org/*
  9. // @require https://cdnjs.cloudflare.com/ajax/libs/spark-md5/3.0.0/spark-md5.js
  10. // @require https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.11/cropper.js
  11. // @resource cropper_css https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.11/cropper.css
  12. // @grant GM_getResourceText
  13. // @grant GM_addStyle
  14. // @icon https://i.imgur.com/hQp5BTf.png
  15. // ==/UserScript==
  16. //
  17. //Using SparkMD5 to generate image hashes - https://github.com/satazor/js-spark-md5
  18. //Using Cropper.js to crop images - https://github.com/fengyuanchen/cropperjs
  19. //
  20. //----------DEBUG MODE-------------//
  21. var DEBUG = false;//console //
  22. //---------------------------------//
  23. const version = 2.3;
  24. //---------------------------------//
  25. if(DEBUG) console.log("[ImageResizer] Initialized");
  26. //CSS
  27. var cssTxt = GM_getResourceText ("cropper_css");
  28. GM_addStyle (cssTxt);
  29. var style = document.createElement("style");
  30. style.innerHTML = '' +
  31. '.centerImg { margin: 0; position: absolute; top: 50%; left: 50%; -ms-transform: translate(-50%, -50%); transform: translate(-50%, -50%); max-width: 100%; max-height: 100vh; height: auto; cursor: pointer; }\n' +
  32. '.settingsOverlay { background: rgba(0,0,0,0.8); display: none; height: 100%; left: 0; position: fixed; top: 0; width: 100%; z-index: 777; } \n' +
  33. '#pvOverlay { background: rgba(0,0,0,0.9); height: 100%; left: 0; position: fixed; top: 0; width: 100%; z-index: 777; text-align: center;} \n' +
  34. '#pvHeader { position: fixed; height: 35px; width: 100%; opacity: 0; -webkit-transition: opacity 0.5s ease-in-out;}\n' +
  35. '#pvHeader:hover { opacity: 0.8; -webkit-transition: none; }\n' +
  36. '.pvOpct { opacity: 0.7 !important; } \n' +
  37. '#imgResizeMenu { position: fixed; top: 20%; left: 35%; width: 30%; min-width: 620px; padding: 2em; overflow: hidden; z-index: 8;}\n' +
  38. '#imgResizeMenu h3 { text-align: center; }\n' +
  39. '#imgResizeMenu a { cursor: pointer; }\n' +
  40. '#imgResizeMenu label { text-decoration-line: underline; }\n' +
  41. '#heplDiv summary { cursor: pointer; }\n' +
  42. '.settingsOverlay input[type=number] { -moz-appearance: textfield; text-align: right; }\n' +
  43. '.resizer-settings { padding-bottom: 5px }\n' +
  44. '#errMsg { color: red; text-align: center; }\n' +
  45. '#ruleTable { border-collapse: collapse; }\n' +
  46. '#ruleTable td, th { padding: 8px; text-align: left; border-bottom: 1pt solid; }\n' +
  47. '#QCTable { border-collapse: collapse; }\n' +
  48. '#QCTable td, th { padding: 8px; text-align: center; border-bottom: 1pt solid; }\n' +
  49. '#QCTable p { margin: auto; max-width: 150px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n' +
  50. '#inputContainer { text-align: center; padding-top: 1em; }\n' +
  51. '#inputContainer button { margin-top: 20px; }\n' +
  52. '.menuBtns { margin-left: 1em; }\n' +
  53. '#sideMenu { position: absolute; display: none; padding: 5px 0px 5px 0px; width: 101px; margin-left: -106px; margin-top: -2px;}\n' +
  54. '.sideMenuElement { background: inherit; display: block; cursor: pointer; padding: 2px 10px 2px 10px; text-align: left;}\n' +
  55. '.downscale-menu-off { display: none; }\n' +
  56. '.downscale-menu-on { display: block !important; }';
  57. var styleRef = document.querySelector("script");
  58. styleRef.parentNode.insertBefore(style, styleRef);
  59. //Load settings
  60. getSettings();
  61. getPresets();
  62. getQCList();
  63. //Update downscale-settings object v2.3
  64. (function () {
  65. if (getSettings().cropOutput == null || getSettings().smoothing == null) {
  66. var settings = getSettings();
  67. settings.cropOutput = "image/jpeg";
  68. settings.smoothing = false;
  69. localStorage.setItem("downscale-settings", JSON.stringify(settings));
  70. var info = '4chan Image Resizer updated to version ' + version + '.\nMore info in Settings > About tab.';
  71. var msgDetail = {type: 'info', content: info, lifetime: 10};
  72. var msgEvent = new CustomEvent('CreateNotification', {bubbles: true, detail: msgDetail});
  73. document.dispatchEvent(msgEvent);
  74. }
  75. })();
  76. function getSettings() {
  77. if (JSON.parse(localStorage.getItem("downscale-settings"))) {
  78. var settings = JSON.parse(localStorage.getItem("downscale-settings"));
  79. }
  80. else {
  81. settings = { enabled:true, notify:true, convert:false, jpegQuality:0.92, shortcut:true , cropOutput:"image/jpeg", smoothing:false};
  82. localStorage.setItem("downscale-settings", JSON.stringify(settings));
  83. }
  84. return settings;
  85. }
  86. function getPresets() {
  87. if (JSON.parse(localStorage.getItem("downscale-presets"))) {
  88. var presets = JSON.parse(localStorage.getItem("downscale-presets"));
  89. }
  90. else {
  91. presets = [];
  92. }
  93. return presets;
  94. }
  95. function getQCList() {
  96. if (JSON.parse(localStorage.getItem("downscale-qclist"))) {
  97. var QCList = JSON.parse(localStorage.getItem("downscale-qclist"));
  98. }
  99. else {
  100. QCList = [];
  101. }
  102. return QCList;
  103. }
  104. //Checking if QuickReply dialog is open.
  105. document.addEventListener('QRDialogCreation', function(listenForQRDC) {
  106. var checkBox = document.getElementById("imgResize");
  107. var sideMenu = document.getElementById("sideMenuArrow");
  108. //Checking if the "resize" check box and "side menu" already exist
  109. if (!sideMenu) {
  110. appendSideMenu();
  111. }
  112. if (!checkBox) {
  113. appendCheckBox();
  114. }
  115. //Listening for clicks on check box
  116. document.getElementById("imgResize").addEventListener("click", checkState);
  117. checkState(1);
  118. if(DEBUG) console.log("[QRFile] Listening...");
  119. //QRFile | Listening for QRFile, in response to: QRGetFile | Request File
  120. document.addEventListener('QRFile', function(GetFile) {
  121. if(DEBUG) console.log("[QRFile] File served: " + GetFile.detail);
  122. //Remove Remember option upon adding a (new) file.
  123. removeRemOption();
  124. const file = GetFile.detail;
  125. //Initialize an instance of a FileReader
  126. const reader = new FileReader();
  127. //Checking if the file is JPG or PNG
  128. if (file.type == "image/jpeg" || file.type == "image/png") {
  129. if(DEBUG) console.log("Acceptable File type: " + file.type);
  130. //add <hr> to sideMenu
  131. var smHR = document.getElementById("sm-hr");
  132. if (!smHR) {
  133. appendHR();
  134. }
  135. //Check if resizer already completed its task (to determine priority)
  136. var complete = false;
  137. var presets = getPresets();
  138. var QCList = getQCList();
  139.  
  140. reader.onload = function(f) {
  141. var img = new Image();
  142. img.src = reader.result;
  143.  
  144. img.onload = function() {
  145. //Base64 MD5 hash of an image
  146. var imgMD5 = SparkMD5.hash(img.src);
  147. if(DEBUG) console.log("<FILTER START>");
  148. if(DEBUG) if(getSettings().convert) console.log("[PNGConverter] Enabled"); else console.log("[PNGConverter] Disabled");
  149. if(DEBUG) console.log("INPUT Dimensions: " + img.width + "x" + img.height);
  150. if(DEBUG) console.log("INPUT File size: " + formatBytes(file.size));
  151. //THE priority list
  152. if (getQCList().length > 0) checkMD5(img, imgMD5);
  153. if (presets.length > 0 && !complete) checkPresets(img);
  154. if (getSettings().convert && !complete) checkPNG(img);
  155. if (!complete) {
  156. //Reset QC and Crop buttons
  157. removeQCOption();
  158. removeCropOption();
  159. quickConvert(img, file, imgMD5);
  160. crop(img);
  161. //Reset preview button
  162. removePreviewOption();
  163. appendPreviewBtn(img.src, file.size, img.width, img.height, file.name);
  164. }
  165. return;
  166. }
  167. return;
  168. }
  169.  
  170. function checkMD5(img, imgMD5) {
  171. if(DEBUG) console.log("[quickConvert] Checking for matching MD5: " + imgMD5);
  172. var filterCount = QCList.length;
  173. var matchFound = false;
  174. for (var i = 0; i < filterCount; i++) {
  175. //unpack md5 hash
  176. var filterMD5 = QCList[i].split(":").pop();
  177. if (filterMD5 == imgMD5) {
  178. if(DEBUG) console.log("[quickConvert] Match found.");
  179. matchFound = true;
  180. resizer(img.width, img.height, img);
  181. break;
  182. }
  183. }
  184. if(DEBUG) if (!matchFound)console.log("[quickConvert] No match found.");
  185. return;
  186. }
  187. function checkPresets(img) {
  188. var matchCount = 0;
  189. var rule = [];
  190. var presetCount = presets.length;
  191. for (var i = 0; i < presetCount; i++) {
  192. //unpack rules
  193. rule[i] = presets[i].split(":");
  194. //check for matching file type
  195. if (rule[i][0] != 0) {
  196. switch (parseInt(rule[i][0])) {
  197. case 1:
  198. rule[i][0] = "image/png";
  199. break;
  200. case 2:
  201. rule[i][0] = "image/jpeg";
  202. }
  203. if (rule[i][0] != file.type) continue;
  204. }
  205. //check for matching dimensions
  206. if (rule[i][1] == img.width && rule[i][2] == img.height) {
  207. var MAX_WIDTH = parseInt(rule[i][3]);
  208. var MAX_HEIGHT = parseInt(rule[i][4]);
  209. matchCount++;
  210. if(DEBUG) console.log("Preset '" + i + "' matched: " + rule[i]);
  211. break;
  212. }
  213. }
  214. //failsafe
  215. if (matchCount == 0 || matchCount > 1) {
  216. if(DEBUG) console.log("Image didn't match any presets.\n------<END>------");
  217. return;
  218. }
  219. else {
  220. resizer(MAX_WIDTH, MAX_HEIGHT, img);
  221. return;
  222. }
  223. }
  224. //PNG -> JPEG
  225. function checkPNG(img) {
  226. if (file.type == "image/png") {
  227. var MAX_WIDTH = img.width;
  228. var MAX_HEIGHT = img.height;
  229. if(DEBUG) console.log("[PNGConverter] Converting PNG to JPEG");
  230. resizer(MAX_WIDTH, MAX_HEIGHT, img);
  231. }
  232. else {
  233. if(DEBUG) console.log("[PNGConverter] Image format isn't PNG.\n------<END>------");
  234. return;
  235. }
  236. }
  237. //The main resize function
  238. function resizer(MAX_WIDTH, MAX_HEIGHT, img, imgMD5) {
  239. if(DEBUG && !imgMD5) console.log("<FILTER END>");
  240. removePreviewOption();
  241. var canvas = document.createElement("canvas");
  242. //Input dimensions
  243. var width = img.width;
  244. var height = img.height;
  245. //Calculating dimensions/aspect ratio
  246. if (width > height) {
  247. if (width > MAX_WIDTH) {
  248. height *= MAX_WIDTH / width;
  249. width = MAX_WIDTH;
  250. }
  251. } else {
  252. if (height > MAX_HEIGHT) {
  253. width *= MAX_HEIGHT / height;
  254. height = MAX_HEIGHT;
  255. }
  256. }
  257. // resize the canvas to the new dimensions
  258. canvas.width = width;
  259. canvas.height = height;
  260. // scale & draw the image onto the canvas
  261. var ctx = canvas.getContext("2d");
  262. ctx.drawImage(img, 0, 0, width, height);
  263. //canvas to dataURL | JPEG quality (0-1)
  264. var dataURL;
  265. if (imgMD5) dataURL = canvas.toDataURL('image/jpeg', 92);
  266. else dataURL = canvas.toDataURL('image/jpeg', parseFloat(getSettings().jpegQuality));
  267. //dataURL to blob
  268. var blob = dataURItoBlob(dataURL);
  269. //Stop classObserver | prevent trigger loop
  270. classObserver.disconnect();
  271. if(DEBUG) console.log("[classObserver] Stopping...");
  272. setFile(blob, img, width, height, imgMD5);
  273. appendPreviewBtn(dataURL, blob.size, width, height, file.name);
  274. }
  275. //Set the new file to QR form
  276. function setFile(blob, img, width, height, imgMD5) {
  277. var new_filename = constructFilename(blob.type, file.name)
  278. var detail = {
  279. file: blob,
  280. name: new_filename
  281. };
  282. var event = new CustomEvent('QRSetFile', {
  283. bubbles: true,
  284. detail: detail
  285. });
  286. document.dispatchEvent(event);
  287. if (imgMD5) rememberQC(img, file, imgMD5, blob.size);
  288. if(DEBUG) console.log("[QRSetFile] File Sent");
  289. if(DEBUG) console.log("OUTPUT Dimesnions: " + Math.round(width) + "x" + Math.round(height));
  290. if(DEBUG) console.log("OUTPUT Filesize: " + formatBytes(blob.size));
  291. if(DEBUG) console.log("JPEG Quality: " + getSettings().jpegQuality);
  292. //Notification
  293. var FSInfo = "Original size: (" + formatBytes(file.size) + ", " + img.width + "x" + img.height + ") \n New size: (" + formatBytes(blob.size)+ ", " + Math.round(width) + "x" + Math.round(height) +")";
  294. if (getSettings().notify) {
  295. var msgDetail = {type: 'info', content: FSInfo, lifetime: 5};
  296. var msgEvent = new CustomEvent('CreateNotification', {bubbles: true, detail: msgDetail});
  297. document.dispatchEvent(msgEvent);
  298. }
  299. //Remove Quick Convert option after conversion
  300. removeQCOption();
  301. removeCropOption();
  302. //Restart classObserver
  303. classObserver.observe(targetNode, observerOptions);
  304. //Preset priority
  305. complete = true;
  306. if(DEBUG) console.log("------<END>------\n[classObserver] Restarting...");
  307. }
  308. //Quick Convert (QC) image, from Side Menu
  309. function quickConvert(img, file, imgMD5) {
  310. //Convert options container (future use)
  311. var container = document.createElement("div");
  312. container.id = "qcDiv";
  313. //Convert button
  314. var convert = document.createElement("a");
  315. convert.id = "quickConvert";
  316. convert.classList.add("sideMenuElement");
  317. convert.classList.add("entry");
  318. convert.innerHTML = "Quick Convert";
  319. convert.title = "Convert image to JPEG";
  320. //CSS on hover
  321. convert.onmouseover = function(){this.classList.toggle("focused")};
  322. convert.onmouseout = function(){this.classList.toggle("focused")};
  323. //Call resizer
  324. convert.addEventListener('click', function(){
  325. if(DEBUG) console.log("[quickConvert] Manually calling Resizer...");
  326. resizer(img.width, img.height, img, imgMD5);
  327. },);
  328. var parent = document.getElementById("sideMenu");
  329. parent.appendChild(container);
  330. container.appendChild(convert);
  331. }
  332.  
  333. //Crop Image button, from Side Menu
  334. function crop(img) {
  335. //Convert options container (future use)
  336. var container = document.createElement("div");
  337. container.id = "cropDiv";
  338. //Convert button
  339. var crop = document.createElement("a");
  340. crop.id = "crop";
  341. crop.classList.add("sideMenuElement");
  342. crop.classList.add("entry");
  343. crop.innerHTML = "Crop Image";
  344. //crop.title = "Crop Image";
  345. //CSS on hover
  346. crop.onmouseover = function(){this.classList.toggle("focused")};
  347. crop.onmouseout = function(){this.classList.toggle("focused")};
  348. //Call cropper
  349. crop.addEventListener('click', function(){
  350. if(DEBUG) console.log("[cropper] Calling cropper...");
  351. cropImage(img);
  352. },);
  353. var parent = document.getElementById("sideMenu");
  354. parent.appendChild(container);
  355. container.appendChild(crop);
  356. }
  357.  
  358. //Image Cropper
  359. function cropImage(img) {
  360. var overlay = document.createElement("div");
  361. overlay.id = "pvOverlay";
  362. //-----------------------------------------------
  363. var header = document.createElement("div");
  364. header.id = "pvHeader";
  365. header.className = "dialog";
  366. header.classList.add("pvOpct");
  367. //header.style = "line-height: 35px;"
  368. //Set Image button-------------------------------
  369. var setBtn = document.createElement("a");
  370. setBtn.id = "setCrop";
  371. setBtn.style.cursor = "pointer";
  372. setBtn.innerHTML = "Set Image";
  373. //Undo button------------------------------------
  374. var undoBtn = document.createElement("a");
  375. undoBtn.id = "undoCrop";
  376. undoBtn.style.cursor = "pointer";
  377. undoBtn.innerHTML = "Undo";
  378. //Close button-----------------------------------
  379. var closeBtn = document.createElement("a");
  380. closeBtn.id = "closeCrop";
  381. closeBtn.className = "close fa fa-times";
  382. closeBtn.style = "float: right; cursor: pointer; margin-right: 20px; margin-top: 7px; transform: scale(1.5);";
  383. closeBtn.title = "Close";
  384. //Cropped <img>----------------------------------
  385. var cropImg = document.createElement("img");
  386. cropImg.id = "cropImg";
  387. cropImg.classList.add("centerImg");
  388. cropImg.src = img.src;
  389. cropImg.title = "LMB: Set\nMMB: Close\nRMB: Undo\nShift+RMB: Context Menu";
  390. cropImg.oncontextmenu = function (){ return false; };
  391. //-----------------------------------------------
  392. document.body.appendChild(overlay);
  393. overlay.appendChild(cropImg);
  394. //Cropper----------------------------------------
  395. let cropper;
  396. const image = document.getElementById('cropImg');
  397. //Do stuff when Cropper is ready
  398. image.addEventListener('ready', function () {
  399. //Scale image to 100%, if it's smaller than overlay/viewport (prevent initial zoom-in/stretching)
  400. if (overlay.clientWidth > img.width && overlay.clientHeight > img.height) {
  401. cropper.zoomTo(1);
  402. if(DEBUG) console.log("[cropper] Scaling image to 100%");
  403. }
  404. var fired = false;
  405. //Cropper function Keybinds
  406. document.body.onkeydown = function (event) {
  407. var e = event || window.event;
  408. if (e.target !== this || !cropper) {
  409. return;
  410. }
  411. if (!fired) {
  412. switch (e.keyCode) {
  413. //Esc or Delete - close Cropper
  414. case 46:
  415. case 27:
  416. e.preventDefault();
  417. cropper.destroy();
  418. overlay.remove();
  419. fired = true;
  420. break;
  421. //Backspace - clear crop selection / undo crop
  422. case 8:
  423. e.preventDefault();
  424. if (document.getElementById('undoCrop')) {
  425. document.getElementById('undoCrop').click();
  426. fired = true;
  427. }
  428. else {
  429. cropper.clear();
  430. }
  431. break;
  432. //Enter or Space - crop image / set image
  433. case 32:
  434. case 13:
  435. e.preventDefault();
  436. if (document.getElementById('setCrop')) {
  437. document.getElementById('setCrop').click();
  438. fired = true;
  439. }
  440. else
  441. {
  442. //get cropped canvas
  443. if(DEBUG) console.log("[cropper] Smoothing: " + getSettings().smoothing);
  444. var croppedCanvas = cropper.getCroppedCanvas({
  445. imageSmoothingEnabled: getSettings().smoothing,
  446. imageSmoothingQuality: 'high',
  447. });
  448. //convert canvas to blob ('image/jpeg', 1)
  449. if(DEBUG) console.log("[cropper] Output format: " + getSettings().cropOutput);
  450.  
  451. var dataURL = croppedCanvas.toDataURL(getSettings().cropOutput, parseFloat(getSettings().jpegQuality));
  452. var blob = dataURItoBlob(dataURL);
  453. //get croping data (dimensions) [rounded]
  454. var cropData = cropper.getData(true);
  455. //kill cropper instance
  456. cropper.destroy();
  457. //show cropped image
  458. cropImg.src = dataURL;
  459. cropImg.addEventListener('mouseup', logMouseButton);
  460. //show header when done
  461. overlay.appendChild(header);
  462. header.appendChild(closeBtn);
  463. header.innerHTML += "Cropped Image (" + formatBytes(blob.size)+ ", " + cropData.width + "x" + cropData.height + ")<br>";
  464. header.appendChild(setBtn);
  465. header.innerHTML += " | ";
  466. header.appendChild(undoBtn);
  467. setTimeout(function() { header.classList.toggle("pvOpct"); }, 2000);
  468. document.getElementById('closeCrop').onclick = function() { cropper.destroy(); overlay.remove(); fired = true; };
  469. document.getElementById('setCrop').onclick = function() { setImage(); };
  470. document.getElementById('undoCrop').onclick = function() { overlay.remove(); cropImage(img); };
  471. }
  472. break;
  473. }
  474. function setImage() {
  475. //Stop classObserver | prevent trigger loop
  476. classObserver.disconnect();
  477. fired = true;
  478. if(DEBUG) console.log("[classObserver] Stopping...");
  479. overlay.remove();
  480. removePreviewOption();
  481. var new_filename = constructFilename(blob.type, file.name)
  482. appendPreviewBtn(dataURL, blob.size, cropImg.width, cropImg.height, new_filename);
  483. setFile(blob, img, cropImg.width, cropImg.height)
  484. }
  485. //Mouse controls
  486. function logMouseButton(e) {
  487. if (typeof e === 'object') {
  488. switch (e.button) {
  489. case 0:
  490. setImage();
  491. break;
  492. case 1:
  493. cropper.destroy(); overlay.remove(); fired = true;
  494. break;
  495. case 2:
  496. if (!e.shiftKey){
  497. overlay.remove(); cropImage(img);
  498. }
  499. break;
  500. }
  501. }
  502. }
  503. }
  504. }
  505. });
  506. //call Cropper with settings
  507. cropper = new Cropper(image, {
  508. aspectRatio: NaN,
  509. background: false,
  510. guides: false,
  511. viewMode: 1,
  512. autoCrop: false,
  513. scalable: false,
  514. });
  515.  
  516. }
  517.  
  518. //Remember button
  519. function rememberQC (img, file, imgMD5, newSize) {
  520. var container = document.createElement("div");
  521. container.id = "remDiv";
  522. var remember = document.createElement("a");
  523. remember.id = "rememberMD5";
  524. remember.classList.add("sideMenuElement");
  525. remember.classList.add("entry");
  526. remember.innerHTML = "Remember";
  527. remember.style.fontWeight = "bold";
  528. remember.title = "Always convert this image."
  529. //CSS on hover
  530. remember.onmouseover = function(){this.classList.toggle("focused")};
  531. remember.onmouseout = function(){this.classList.toggle("focused")};
  532. remember.onclick = function(){ saveImgMD5(img, file, imgMD5, newSize) };
  533. var parent = document.getElementById("sideMenu");
  534. parent.appendChild(container);
  535. container.appendChild(remember);
  536. }
  537. //Preview Image button
  538. function appendPreviewBtn(img, pvSize, pvWidth, pvHeight, pvName) {
  539. var existCheck = document.getElementById("previewImg");
  540. if (!existCheck) {
  541. var preview = document.createElement("a");
  542. preview.id = "previewImg";
  543. preview.classList.add("sideMenuElement");
  544. preview.classList.add("entry");
  545. preview.innerHTML = "Preview Image";
  546. //CSS on hover
  547. preview.onmouseover = function(){this.classList.toggle("focused")};
  548. preview.onmouseout = function(){this.classList.toggle("focused")};
  549. preview.onclick = function(){ showImage(img, pvSize, pvWidth, pvHeight, pvName) };
  550. var parent = document.getElementById("sideMenu");
  551. parent.appendChild(preview);
  552. }
  553. else {
  554. existCheck.onclick = function(){ showImage(img, pvSize, pvWidth, pvHeight, pvName) };
  555. }
  556. return;
  557. }
  558. //Read the file
  559. reader.readAsDataURL(file);
  560. } else {
  561. removeHR();
  562. removeCropOption();
  563. removeQCOption();
  564. removePreviewOption();
  565. if(DEBUG) console.log("[Error] Invalid FileType: " + file.type + "\n------<END>------");
  566. }
  567. }, false);
  568. //Observing if a file was uploaded or not | checking if div (with id: "file-n-submit") has class named: "has-file"
  569. function callback(mutationList, observer) {
  570. if (document.getElementById("file-n-submit").classList.contains("has-file") === true && checkState(2) === true) {
  571. if(DEBUG) console.log("------<START>------\n[classObserver] File detected")
  572. //QRGetFile | Request File
  573. if(DEBUG) console.log("[QRGetFile] Requesting file...");
  574. document.dispatchEvent(new CustomEvent('QRGetFile'));
  575.  
  576. } else if (checkState(2) === false) {
  577. if(DEBUG) console.log("[classObserver] ImageResizer is disabled");
  578. return;
  579. }
  580. else {
  581. //Remove Side menu options upon removing a file.
  582. removeHR();
  583. removeCropOption();
  584. removeQCOption();
  585. removeRemOption();
  586. removePreviewOption();
  587. if(DEBUG) console.log("[classObserver] No file");
  588. }
  589. }
  590. //MutationObserver. Checks if div (with id "file-n-submit") has its class attribute changed
  591. const targetNode = document.getElementById("file-n-submit");
  592. var observerOptions = {
  593. attributes: true
  594. };
  595. var classObserver = new MutationObserver(callback);
  596. if(DEBUG) console.log("[classObserver] Starting...");
  597. classObserver.observe(targetNode, observerOptions);
  598. }, false);
  599. //*************************************************************************************//
  600. //END OF THE MAIN PROCESS
  601. //*************************************************************************************//
  602. //Add a label with a check box for ImageResize + Setting button in Side Menu
  603. function appendCheckBox() {
  604. var settingsButton = document.createElement("a");
  605. var label = document.createElement("label");
  606. var input = document.createElement("input");
  607. input.type = "checkbox";
  608. input.id = "imgResize";
  609. label.id = "imgResizeLabel";
  610. input.title = "Enable Image Resizer";
  611. input.style = "margin-left: 0";
  612. settingsButton.classList.add("sideMenuElement");
  613. settingsButton.classList.add("entry");
  614. label.classList.add("sideMenuElement");
  615. //CSS on hover
  616. label.classList.add("entry");
  617. var parent = document.getElementById("sideMenu");
  618. parent.appendChild(label);
  619. label.appendChild(input);
  620. label.title = "Enable Image Resizer";
  621. label.innerHTML += " Enabled";
  622. settingsButton.title = "Image Resizer Settings";
  623. settingsButton.innerHTML = "Settings";
  624. parent.appendChild(settingsButton);
  625. //CSS on hover
  626. label.onmouseover = function(){this.classList.toggle("focused")};
  627. label.onmouseout = function(){this.classList.toggle("focused")};
  628. settingsButton.onmouseover = function(){this.classList.toggle("focused")};
  629. settingsButton.onmouseout = function(){this.classList.toggle("focused")};
  630. //Open settings menu
  631. settingsButton.onclick = function(){ document.getElementById("imgResizeOverlay").style.display = "block" };
  632. //Checked by default
  633. document.getElementById("imgResize").checked = getSettings().enabled;
  634. }
  635. //Check box state
  636. function checkState(caller) {
  637. var state = document.getElementById("imgResize").checked;
  638. if (state === true) {
  639. if (caller != 2) if(DEBUG) console.log("[ImageResizer] Enabled");
  640. return true;
  641. } else {
  642. if (caller != 2) if(DEBUG) console.log("[ImageResizer] Disabled");
  643. //remove side menu options upon disabling ImageResizer
  644. removeHR(); removeCropOption(); removeQCOption(); removeRemOption(); removePreviewOption();
  645. return false;
  646. }
  647. }
  648. //Clears error messages <p>
  649. function clearErr() { document.getElementById("errMsg").innerHTML = ""; }
  650. //Checks for any logic errors (upscaling)
  651. function basicCheck(edit, rulePos) {
  652. var inWidth = parseInt(document.getElementById("inWidth").value);
  653. var inHeight = parseInt(document.getElementById("inHeight").value);
  654. var outWidth = parseInt(document.getElementById("outWidth").value);
  655. var outHeight = parseInt(document.getElementById("outHeight").value);
  656. var imgType = parseInt(document.getElementById("imgType").value);
  657. if (outWidth <= 0 || outHeight <= 0) { document.getElementById("errMsg").innerHTML = "Invalid output dimensions"; return}
  658. else if (inWidth < outWidth || inHeight < outHeight) { document.getElementById("errMsg").innerHTML = "Cannot upscale images"; return}
  659. else finalCheck(edit, imgType, inWidth, inHeight, outWidth, outHeight, rulePos);
  660. return;
  661. }
  662. //Checks for any rule overlaps
  663. // ([0] - Image type, [1] - Input width, [2] - Input height, [3] - Output width, [4] - Output height)
  664. function finalCheck(edit, imgType, inWidth, inHeight, outWidth, outHeight, rulePos) {
  665. var e = document.getElementById("imgType");
  666. var format = e.options[e.selectedIndex].text;
  667. var presetString = imgType + ":" + inWidth + ":" + inHeight + ":" + outWidth + ":" + outHeight;
  668. var presets = getPresets();
  669. if (presets.length > 0) {
  670. var rule = [];
  671. var presetCount = presets.length;
  672. for (var i = 0; i < presetCount; i++) {
  673. if (edit && i === rulePos) continue;
  674. rule[i] = presets[i].split(":");
  675. if (presetString == presets[i]) { document.getElementById("errMsg").innerHTML = "Exact preset already exists"; return }
  676. else if ((inWidth == rule[i][1] && inHeight == rule[i][2]) && (imgType == rule[i][0] || rule[i][0] == 0)) { document.getElementById("errMsg").innerHTML = "Preset with the same input dimensions for " + format + " format already exists"; return }
  677. }
  678. }
  679. //save preset
  680. clearErr();
  681. if (edit) presets[rulePos] = presetString;
  682. else presets.push(presetString);
  683. localStorage.setItem("downscale-presets", JSON.stringify(presets));
  684. //rebuild list
  685. document.getElementById("ruleTable").tBodies.item(0).innerHTML = "";
  686. printList();
  687. //hide / display
  688. document.getElementById("ruleInput").remove();
  689. document.getElementById("addRule").style.display = "inline";
  690. return;
  691. }
  692. //Check if possible to calculate output WIDTH
  693. function aspectCheckH() {
  694. var inWidth = document.getElementById("inWidth").value;
  695. var inHeight = document.getElementById("inHeight").value;
  696. var outWidth = document.getElementById("outWidth").value;
  697. var outHeight = document.getElementById("outHeight").value;
  698. if (outHeight > 0) {
  699. if (parseInt(inHeight) >= parseInt(outHeight)) {
  700. calcAspect("width", inWidth, inHeight, outHeight);
  701. clearErr();
  702. }
  703. else {
  704. document.getElementById("errMsg").innerHTML = "Cannot upscale images";
  705. }
  706. }
  707. }
  708. //Check if possible to calculate output HEIGHT
  709. function aspectCheckW() {
  710. var inWidth = document.getElementById("inWidth").value;
  711. var inHeight = document.getElementById("inHeight").value;
  712. var outWidth = document.getElementById("outWidth").value;
  713. var outHeight = document.getElementById("outHeight").value;
  714. if (outWidth > 0) {
  715. if (parseInt(inWidth) >= parseInt(outWidth)) {
  716. calcAspect("height", inWidth, inHeight, outWidth);
  717. clearErr();
  718. }
  719. else {
  720. document.getElementById("errMsg").innerHTML = "Cannot upscale images";
  721. }
  722. }
  723. }
  724. //Aspect ratio calculation (finds the other output dimension based on given exact input dimensions)
  725. function calcAspect(dimension, w, h, output) {
  726. if (dimension == "width") {
  727. var width = output / h * w;
  728. document.getElementById("outWidth").value = Math.round(width);
  729. }
  730. if (dimension == "height") {
  731. var height = output / w * h;
  732. document.getElementById("outHeight").value = Math.round(height);
  733. }
  734. }
  735. //Populate Presets list
  736. function printList() {
  737. var presets = getPresets();
  738. var list = document.getElementById("imgResizeList");
  739. var table = document.getElementById("ruleTable");
  740. if (presets.length > 0) {
  741. var rule = [];
  742. var presetCount = presets.length;
  743. for (let i = 0; i < presetCount; i++) {
  744. rule[i] = presets[i].split(":");
  745. switch (parseInt(rule[i][0])) {
  746. case 0:
  747. rule[i][0] = "PNG/JPEG";
  748. break;
  749. case 1:
  750. rule[i][0] = "PNG";
  751. break;
  752. case 2:
  753. rule[i][0] = "JPEG";
  754. }
  755. let delRow = document.createElement("a");
  756. let editRow = document.createElement("a");
  757. delRow.innerHTML = "delete";
  758. editRow.innerHTML = "edit";
  759. //delete a rule and rebuild the list
  760. delRow.onclick = function() {
  761. if (document.getElementById("inputContainer")) document.getElementById("inputContainer").innerHTML = "";
  762. presets.splice(delRow.parentElement.parentElement.sectionRowIndex, 1);
  763. localStorage.setItem("downscale-presets", JSON.stringify(presets));
  764. table.tBodies.item(0).innerHTML = "";
  765. printList();
  766. clearErr();
  767. document.getElementById("addRule").style.display = "inline";
  768. };
  769. editRow.onclick = function() { inputUI(true, rule[i], i); clearErr(); };
  770. //Array contents: [0] - Image type, [1] - Input width, [2] - Input height, [3] - Output width, [4] - Output height
  771. var row = table.tBodies.item(0).insertRow(-1);
  772. row.insertCell(0).innerHTML = rule[i][0];
  773. row.insertCell(1).innerHTML = '[ ' + rule[i][1] + ' x ' + rule[i][2] + ' ]';
  774. row.insertCell(2).innerHTML = '&#8594;';
  775. row.insertCell(3).innerHTML = '[ ' + rule[i][3] + ' x ' + rule[i][4] + ' ]';
  776. row.insertCell(4).appendChild(editRow);
  777. row.insertCell(5).appendChild(delRow);
  778. }
  779. }
  780. }
  781. //Input field
  782. function inputUI(edit, rule, rulePos) {
  783. if (document.getElementById("inputContainer")) document.getElementById("inputContainer").innerHTML = "";
  784. document.getElementById("addRule").style.display = "none";
  785. var inputDiv = document.getElementById("inputContainer");
  786. var input = document.createElement("div");
  787. var discardRuleBtn = document.createElement("button");
  788. discardRuleBtn.innerHTML = "Cancel";
  789. var saveRuleBtn = document.createElement("button");
  790. saveRuleBtn.innerHTML = "Save";
  791. input.id = "ruleInput";
  792. //Rules form
  793. input.innerHTML = '' +
  794. '' +
  795. '<select id="imgType" name="imgType" title="Input Format">' +
  796. '<option value="0">PNG/JPEG</option>' +
  797. '<option value="1">PNG</option>' +
  798. '<option value="2">JPEG</option>' +
  799. '</select>&ensp;' +
  800. '' +
  801. '<input type="number" id="inWidth" title="Input Width" size="2" min="0" value="0" onfocus="this.select();"></input> x ' +
  802. '' +
  803. '<input type="number" id="inHeight" title="Input Height" size="2" min="0" value="0" onfocus="this.select();"></input> ' +
  804. '&ensp; &#8594; &ensp; <input type="number" id="outWidth" title="Output Width" size="2" min="0" value="0" onfocus="this.select();"></input> x ' +
  805. '<input type="number" id="outHeight" title="Output Height" size="2" min="0" value="0" onfocus="this.select();"></input><br>';
  806. inputDiv.appendChild(input);
  807. var inWidth = document.getElementById("inWidth");
  808. var inHeight = document.getElementById("inHeight");
  809. var outWidth = document.getElementById("outWidth");
  810. var outHeight = document.getElementById("outHeight");
  811. if (edit) {
  812. switch (rule[0]) {
  813. case "PNG/JPEG":
  814. document.getElementById("imgType").selectedIndex = 0;
  815. break;
  816. case "PNG":
  817. document.getElementById("imgType").selectedIndex = 1;
  818. break;
  819. case "JPEG":
  820. document.getElementById("imgType").selectedIndex = 2;
  821. }
  822. inWidth.value = rule[1];
  823. inHeight.value = rule[2];
  824. outWidth.value = rule[3];
  825. outHeight.value = rule[4];
  826. }
  827. //Listen for user input on target dimension input fields to automatically calculate aspect ratio
  828. outWidth.addEventListener("input", aspectCheckW);
  829. outHeight.addEventListener("input", aspectCheckH);
  830. inWidth.onkeypress = function() { outHeight.value = 0; outWidth.value = 0; return isNumber(event); };
  831. inHeight.onkeypress = function() { outHeight.value = 0; outWidth.value = 0; return isNumber(event); };
  832. outWidth.onkeypress = function() { return isNumber(event); };
  833. outHeight.onkeypress = function() { return isNumber(event); };
  834.  
  835. input.appendChild(saveRuleBtn);
  836. input.appendChild(discardRuleBtn);
  837. discardRuleBtn.onclick = function(){ document.getElementById(input.id).remove(); document.getElementById("addRule").style.display = "inline"; clearErr();};
  838. saveRuleBtn.onclick = function() { if (edit) basicCheck(true, rulePos); else basicCheck(false); };
  839. }
  840. //Populate Quick Convert List table
  841. function printQCList() {
  842. var QCList = getQCList();
  843. var list = document.getElementById("QCList");
  844. var table = document.getElementById("QCTable");
  845. var filterCount = QCList.length;
  846. if (filterCount > 0) {
  847. var QCFilter = [];
  848. for (let i = 0; i < filterCount; i++) {
  849. QCFilter[i] = QCList[i].split(":");
  850. let delRow = document.createElement("a");
  851. delRow.innerHTML = "delete";
  852. delRow.onclick = function() {
  853. QCList.splice(delRow.parentElement.parentElement.sectionRowIndex, 1);
  854. localStorage.setItem("downscale-qclist", JSON.stringify(QCList));
  855. table.tBodies.item(0).innerHTML = "";
  856. printQCList();
  857. };
  858. //QCList Array: [0] - Filetype, [1] - Image Width, [2] - Image Height, [3] - Original Filesize, [4] - New Filesize, [5] - Filename, [6] - Image Base64 MD5 Hash
  859. var row = table.tBodies.item(0).insertRow(-1);
  860. row.insertCell(0).innerHTML = QCFilter[i][0];
  861. row.insertCell(1).innerHTML = '[ ' + QCFilter[i][1] + ' x ' + QCFilter[i][2] + ' ]';
  862. row.insertCell(2).innerHTML = QCFilter[i][3];
  863. row.insertCell(3).innerHTML = '&#8594;';
  864. row.insertCell(4).innerHTML = QCFilter[i][4];
  865. row.insertCell(5).innerHTML = '<p title = "' + QCFilter[i][5] +'">' + QCFilter[i][5] + '</p>';
  866. row.insertCell(6).appendChild(delRow);
  867. }
  868. }
  869. }
  870. //*************************************************************************************//
  871. // MENUS //
  872. //*************************************************************************************//
  873. function appendSettings() {
  874. //Button--------------------------------------------------------
  875. var span = document.createElement("span");
  876. var button = document.createElement("a");
  877. button.id = "imgResizeSettings";
  878. button.className += "fa fa-cog";
  879. button.style = "cursor: pointer;";
  880. button.title = "Image Resizer Settings";
  881. var ref = document.getElementById('shortcut-settings');
  882. ref.insertBefore(span, parent.nextSibling);
  883. span.appendChild(button);
  884. //Overlay | imgResizeOverlay------------------------------------
  885. var overlay = document.createElement("div");
  886. overlay.id = "imgResizeOverlay";
  887. overlay.classList.add("settingsOverlay");
  888. document.body.appendChild(overlay);
  889. //Settings menu links | imgResizeMenu---------------------------
  890. var menu = document.createElement("div");
  891. menu.id = "imgResizeMenu";
  892. menu.classList.add("dialog");
  893. overlay.appendChild(menu);
  894. var close = document.createElement("a");
  895. close.className += "close fa fa-times";
  896. close.style = "float: right;";
  897. close.title = "Close";
  898. menu.insertAdjacentElement('afterbegin', close);
  899. //Settings
  900. var settingsBtn = document.createElement("a");
  901. settingsBtn.innerHTML += "Settings";
  902. settingsBtn.classList.add("menuBtns");
  903. settingsBtn.style = "font-weight: bold;";
  904. settingsBtn.onclick = function() {
  905. settingsDiv.className = "downscale-menu-on";
  906. presetsDiv.className = "downscale-menu-off";
  907. QCListDiv.className = "downscale-menu-off";
  908. helpDiv.className = "downscale-menu-off";
  909. settingsBtn.style = "font-weight: bold;";
  910. presetsBtn.style = "";
  911. QCListBtn.style = "";
  912. helpBtn.style = "";
  913. };
  914. menu.appendChild(settingsBtn);
  915. //Presets
  916. var presetsBtn = document.createElement("a");
  917. presetsBtn.innerHTML += "Presets";
  918. presetsBtn.classList.add("menuBtns");
  919. presetsBtn.onclick = function() {
  920. settingsDiv.className = "downscale-menu-off";
  921. presetsDiv.className = "downscale-menu-on";
  922. QCListDiv.className = "downscale-menu-off";
  923. helpDiv.className = "downscale-menu-off";
  924. settingsBtn.style = "";
  925. presetsBtn.style = "font-weight: bold;";
  926. QCListBtn.style = "";
  927. helpBtn.style = "";
  928. };
  929. menu.appendChild(presetsBtn);
  930. //Quick Convert List
  931. var QCListBtn = document.createElement("a");
  932. QCListBtn.innerHTML += "Quick Convert";
  933. QCListBtn.classList.add("menuBtns");
  934. QCListBtn.onclick = function() {
  935. settingsDiv.className = "downscale-menu-off";
  936. presetsDiv.className = "downscale-menu-off";
  937. QCListDiv.className = "downscale-menu-on";
  938. helpDiv.className = "downscale-menu-off";
  939. settingsBtn.style = "";
  940. presetsBtn.style = "";
  941. QCListBtn.style = "font-weight: bold;";
  942. helpBtn.style = "";
  943. };
  944. menu.appendChild(QCListBtn);
  945. //Help
  946. var helpBtn = document.createElement("a");
  947. helpBtn.innerHTML += "About";
  948. helpBtn.classList.add("menuBtns");
  949. helpBtn.onclick = function() {
  950. settingsDiv.className = "downscale-menu-off";
  951. presetsDiv.className = "downscale-menu-off";
  952. QCListDiv.className = "downscale-menu-off";
  953. helpDiv.className = "downscale-menu-on";
  954. settingsBtn.style = "";
  955. presetsBtn.style = "";
  956. QCListBtn.style = "";
  957. helpBtn.style = "font-weight: bold;";
  958. };
  959. menu.appendChild(helpBtn);
  960. var hr = document.createElement("hr");
  961. hr.style.borderColor = getHRColor();
  962. menu.appendChild(hr);
  963. //Content divs| imgResizeContent---------------------------------
  964. var content = document.createElement("div");
  965. content.id = "imgResizeContent";
  966. menu.appendChild(content);
  967. content.innerHTML = "";
  968. var errMsg = document.createElement("p");
  969. errMsg.id = "errMsg";
  970. //Settings
  971. var settingsDiv = document.createElement("div");
  972. settingsDiv.id = "settingsDiv";
  973. settingsDiv.classList.add("downscale-menu-on");
  974. content.appendChild(settingsDiv);
  975. //Presets
  976. var presetsDiv = document.createElement("div");
  977. presetsDiv.id = "presetsDiv";
  978. presetsDiv.classList.add("downscale-menu-off");
  979. presetsDiv.style.textAlign = "center";
  980. content.appendChild(presetsDiv);
  981. //Quick Convert List
  982. var QCListDiv = document.createElement("div");
  983. QCListDiv.id = "QCListDiv";
  984. QCListDiv.classList.add("downscale-menu-off");
  985. content.appendChild(QCListDiv);
  986. //Help
  987. var helpDiv = document.createElement("div");
  988. helpDiv.id = "heplDiv";
  989. helpDiv.classList.add("downscale-menu-off");
  990. content.appendChild(helpDiv);
  991. //--------------------------------------------------------------
  992. var title = document.createElement("h3");
  993. title.innerHTML = "Image Resizer Settings";
  994. settingsDiv.appendChild(title);
  995. //Enable Resizer------------------------------------------------
  996. var enableDiv = document.createElement("div");
  997. enableDiv.classList.add("resizer-settings");
  998. enableDiv.innerHTML = '' +
  999. '<input type="checkbox" id="enableSet" title="" size="1"></input>' +
  1000. '<label for="enableSet">Enable Resizer</label>:&ensp;' +
  1001. 'Enable 4chan Image Resizer by default.';
  1002. settingsDiv.appendChild(enableDiv);
  1003. var enableSet = document.getElementById("enableSet");
  1004. enableSet.checked = getSettings().enabled;
  1005. enableSet.oninput = function() {
  1006. //remove side menu options upon disabling ImageResizer
  1007. if (!enableSet.checked) { removeCropOption(); removeQCOption(); removeRemOption(); removePreviewOption(); }
  1008. var settings = getSettings();
  1009. settings.enabled = enableSet.checked;
  1010. document.getElementById("imgResize").checked = enableSet.checked;
  1011. localStorage.setItem("downscale-settings", JSON.stringify(settings));
  1012. };
  1013. //Enable Shortcut-----------------------------------------------
  1014. var shortcutDiv = document.createElement("div");
  1015. shortcutDiv.classList.add("resizer-settings");
  1016. shortcutDiv.innerHTML = '' +
  1017. '<input type="checkbox" id="shortcutSet" title="" size="1"></input>' +
  1018. '<label for="shortcutSet">Enable Shortcut</label>:&ensp;' +
  1019. 'Enable "Quick Convert" shortcut. <kbd>Ctrl</kbd> + <kbd>Q</kbd>';
  1020. settingsDiv.appendChild(shortcutDiv);
  1021. var shortcutSet = document.getElementById("shortcutSet");
  1022. shortcutSet.checked = getSettings().shortcut;
  1023. shortcutSet.oninput = function() {
  1024. var settings = getSettings();
  1025. settings.shortcut = shortcutSet.checked;
  1026. localStorage.setItem("downscale-settings", JSON.stringify(settings));
  1027. };
  1028. //Display notifications-----------------------------------------
  1029. var notifySetDiv = document.createElement("div");
  1030. notifySetDiv.classList.add("resizer-settings");
  1031. notifySetDiv.innerHTML = '' +
  1032. '<input type="checkbox" id="displaySet" title="" size="1"></input>' +
  1033. '<label for="displaySet">Display Notifications</label>:&ensp;' +
  1034. 'Display a notification when an image is downscaled.';
  1035. settingsDiv.appendChild(notifySetDiv);
  1036. var notifySet = document.getElementById('displaySet');
  1037. notifySet.checked = getSettings().notify;
  1038. notifySet.oninput = function() {
  1039. var settings = getSettings();
  1040. settings.notify = notifySet.checked;
  1041. localStorage.setItem("downscale-settings", JSON.stringify(settings));
  1042. };
  1043. //Convert all PNGs to JPEGs-------------------------------------
  1044. var convertSetDiv = document.createElement("div");
  1045. convertSetDiv.classList.add("resizer-settings");
  1046. convertSetDiv.innerHTML = '' +
  1047. '<input type="checkbox" id="convertSet" title="" size="1"></input>' +
  1048. '<label for="convertSet">Convert All PNGs</label>:&ensp;' +
  1049. 'Automatically convert all added PNGs to JPEGs. Presets apply as normal.';
  1050. settingsDiv.appendChild(convertSetDiv);
  1051. var convertSet = document.getElementById('convertSet');
  1052. convertSet.checked = getSettings().convert;
  1053. convertSet.oninput = function() {
  1054. var settings = getSettings();
  1055. settings.convert = convertSet.checked;
  1056. localStorage.setItem("downscale-settings", JSON.stringify(settings));
  1057. };
  1058. //Set JPEG quality----------------------------------------------
  1059. //RegExp ^(0(\.\d{1,2})?|1(\.0+)?)$
  1060. //Only one number (0 or 1) before decimal, and up tp 2 numbers after decimal, if there is a 0 before decimal (between 0 and 9)
  1061. //e.g. 0.92 true, 1.92 false
  1062. var qualitySetDiv = document.createElement("div");
  1063. qualitySetDiv.classList.add("resizer-settings");
  1064. qualitySetDiv.innerHTML = '' +
  1065. '<input type="text" id="imgQuality" title="JPEG Quality" size="1"></input>' +
  1066. '<label for="imgQuality">JPEG Quality</label>:&ensp;' +
  1067. 'A number between 0 and 1 indicating the output image quality.';
  1068. settingsDiv.appendChild(qualitySetDiv);
  1069. var inputField = document.getElementById('imgQuality');
  1070. inputField.value = getSettings().jpegQuality;
  1071. inputField.onkeypress = function() { return isDecimalNumber(event); };
  1072. //Check input field validity
  1073. inputField.oninput = function() {
  1074. var inputField = document.getElementById('imgQuality');
  1075. var r = new RegExp(/^(0(\.\d{1,2})?|1(\.0+)?)$/);
  1076. if(r.test(document.getElementById('imgQuality').value)) {
  1077. inputField.setCustomValidity("");
  1078. var settings = getSettings();
  1079. settings.jpegQuality = inputField.value;
  1080. localStorage.setItem("downscale-settings", JSON.stringify(settings));
  1081. }
  1082. else inputField.setCustomValidity("Set the value between 1 and 0 up to 2 numbers after the decimal point.");
  1083. };
  1084. //Cropper Settings-----------------------------------------------
  1085. var title2 = document.createElement("h3");
  1086. title2.innerHTML = "Image Cropper Settings";
  1087. settingsDiv.appendChild(title2);
  1088. //Crop output format---------------------------------------------
  1089. var cropOutDiv = document.createElement("div");
  1090. cropOutDiv.classList.add("resizer-settings");
  1091. cropOutDiv.innerHTML = '' +
  1092. '<select id="cropOut" name="cropOut">' +
  1093. '<option value="image/png">PNG</option>' +
  1094. '<option value="image/jpeg">JPEG</option>' +
  1095. '</select>&ensp;' +
  1096. '<label for="cropOut">Output Format</label>:&ensp;' +
  1097. 'Set the desired output format for cropped images.';
  1098. settingsDiv.appendChild(cropOutDiv);
  1099. var cropOutSet = document.getElementById('cropOut');
  1100. cropOutSet.value = getSettings().cropOutput;
  1101. cropOutSet.oninput = function() {
  1102. var settings = getSettings();
  1103. settings.cropOutput = cropOutSet.value;
  1104. localStorage.setItem("downscale-settings", JSON.stringify(settings));
  1105. };
  1106. //Crop image smoothing------------------------------------------
  1107. var cropSmoothDiv = document.createElement("div");
  1108. cropSmoothDiv.classList.add("resizer-settings");
  1109. cropSmoothDiv.innerHTML = '' +
  1110. '<input type="checkbox" id="cropSmooth" title="" size="1"></input>' +
  1111. '<label for="cropSmooth">Enable Smoothing</label>:&ensp;' +
  1112. 'Enables image smoothing for cropped images. Use this for sharp images.';
  1113. settingsDiv.appendChild(cropSmoothDiv);
  1114. var smoothSet = document.getElementById('cropSmooth');
  1115. smoothSet.checked = getSettings().smoothing;
  1116. smoothSet.oninput = function() {
  1117. var settings = getSettings();
  1118. settings.smoothing = smoothSet.checked;
  1119. localStorage.setItem("downscale-settings", JSON.stringify(settings));
  1120. };
  1121. //Preset table | ruleTable----------------------------------------
  1122. var tableWrapper = document.createElement("div");
  1123. tableWrapper.style.overflowY = "auto";
  1124. tableWrapper.style.maxHeight = "220px";
  1125. var table = document.createElement("table");
  1126. var thead = document.createElement("thead");
  1127. var tbody = document.createElement("tbody");
  1128. var presetsTitle = document.createElement("h3");
  1129. presetsTitle.innerHTML = "Presets";
  1130. presetsDiv.appendChild(presetsTitle);
  1131. table.appendChild(thead);
  1132. table.appendChild(tbody);
  1133. table.id = "ruleTable";
  1134. var row = thead.insertRow(0);
  1135. row.insertCell(0).outerHTML = "<th>Format</th>";
  1136. row.insertCell(1).outerHTML = "<th>Input</th>";
  1137. row.insertCell(2).outerHTML = "<th></th>";
  1138. row.insertCell(3).outerHTML = "<th>Output</th>";
  1139. row.insertCell(4).outerHTML = "<th></th>";
  1140. row.insertCell(5).outerHTML = "<th></th>";
  1141. presetsDiv.appendChild(tableWrapper);
  1142. tableWrapper.appendChild(table);
  1143. //Input container | inputContainer------------------------------
  1144. var inputDiv = document.createElement("div");
  1145. inputDiv.id = "inputContainer";
  1146. presetsDiv.appendChild(inputDiv);
  1147. var addRuleBtn = document.createElement("button");
  1148. addRuleBtn.id = "addRule";
  1149. addRuleBtn.innerHTML = "New Preset";
  1150. printList();
  1151. presetsDiv.appendChild(addRuleBtn);
  1152. presetsDiv.appendChild(errMsg);
  1153. button.onclick = function(){ overlay.style.display = "block"; };
  1154. close.onclick = function(){ overlay.style.display = "none"; };
  1155. window.addEventListener('click', function(closeSettingsMenu) {
  1156. if (closeSettingsMenu.target == overlay) overlay.style.display = "none";
  1157. });
  1158. addRuleBtn.onclick = function(){ inputUI(false); };
  1159. //import/export buttons
  1160. var bottomPresets = document.createElement("div");
  1161. bottomPresets.style = "float: left;";
  1162. var separator1 = document.createElement("span");
  1163. separator1.innerHTML = " | ";
  1164. var importPresets = document.createElement("a");
  1165. var exportPresets = document.createElement("a");
  1166. importPresets.innerHTML = "Import";
  1167. exportPresets.innerHTML = "Export";
  1168. importPresets.classList.add("menuBtns");
  1169. bottomPresets.innerHTML += '<input id="importPresetsFile-input" type="file" accept=".json" style="display: none;" />'; //file-input
  1170. importPresets.onclick = function(){
  1171. document.getElementById('importPresetsFile-input').click();
  1172. };
  1173. exportPresets.onclick = function(){ downloadObjectAsJson(getPresets(), "4chan Image Resizer v" + version + " Presets List - " + Date.now()); }; //call file exporter
  1174. bottomPresets.appendChild(importPresets);
  1175. bottomPresets.appendChild(separator1);
  1176. bottomPresets.appendChild(exportPresets);
  1177. presetsDiv.appendChild(bottomPresets);
  1178. //import
  1179. document.getElementById('importPresetsFile-input').addEventListener('change', function() {
  1180. var jsonPresetsFile = new FileReader();
  1181. jsonPresetsFile.onload = function() {
  1182. var originalPresets = getPresets();
  1183. var duplicateCount1 = 0;
  1184. var tempDuplicateCount1 = 0;
  1185. //parse raw text
  1186. var importedPresets = JSON.parse(jsonPresetsFile.result);
  1187. //check if array
  1188. if (Array.isArray(importedPresets)) {
  1189. for (let i = 0; i < importedPresets.length; i++) {
  1190. var line1 = importedPresets[i].split(':');
  1191. if (line1.length != 5) {
  1192. if(DEBUG) console.log("[Error] Imported array does not match the required length (5)");
  1193. if(DEBUG) console.log(line1);
  1194. alert("Error: Array length mismatch.\nThis file is either outdated or invalid.");
  1195. return;
  1196. }
  1197. else {
  1198. //check for duplicate entries
  1199. for (let j = 0; j < originalPresets.length; j++) {
  1200. var tempLine = line1[0] + ":" + line1[1] + ":" + line1[2] + ":" + line1[3] + ":" + line1[4];
  1201. if (tempLine == originalPresets[j]) {
  1202. tempDuplicateCount1++;
  1203. break;
  1204. }
  1205. }
  1206. //if not a dupe, push to the original array
  1207. if (tempDuplicateCount1 == 0) {
  1208. originalPresets.push(importedPresets[i]);
  1209. }
  1210. //count all duplicate entries
  1211. else {
  1212. duplicateCount1 += tempDuplicateCount1;
  1213. tempDuplicateCount1 = 0;
  1214. }
  1215. }
  1216. }
  1217. //add the final result to local storage
  1218. localStorage.setItem("downscale-presets", JSON.stringify(originalPresets));
  1219. //rebuild list
  1220. document.getElementById("ruleTable").tBodies.item(0).innerHTML = "";
  1221. printList();
  1222. var newEntries1 = importedPresets.length - duplicateCount1;
  1223. alert("Succesfully imported " + importedPresets.length + " entries.\nDuplicate entries skipped: " + duplicateCount1 + "\nNew entries added: " + newEntries1);
  1224. }
  1225. else {
  1226. alert("Error: Invalid data type.");
  1227. if(DEBUG) console.log("[Error] Imported data object is not an array.")
  1228. }
  1229. }
  1230. jsonPresetsFile.readAsText(this.files[0]);
  1231. });
  1232. //Quick Convert table | QCTable----------------------------------
  1233. var QCTableWrapper = document.createElement("div");
  1234. QCTableWrapper.style.overflowY = "auto";
  1235. QCTableWrapper.style.maxHeight = "220px";
  1236. var QCTable = document.createElement("table");
  1237. var QCThead = document.createElement("thead");
  1238. var QCTbody = document.createElement("tbody");
  1239. var QCTitle = document.createElement("h3");
  1240. QCTitle.innerHTML = "Quick Convert List";
  1241. QCListDiv.appendChild(QCTitle);
  1242. QCListDiv.innerHTML += "<p style='text-align: center;'>Images on this list will be automatically converted to JPEG with a quality setting of 92.</p>";
  1243. QCTable.appendChild(QCThead);
  1244. QCTable.appendChild(QCTbody);
  1245. QCTable.id = "QCTable";
  1246. var QCRow = QCThead.insertRow(0);
  1247. QCRow.insertCell(0).outerHTML = "<th>Format</th>";
  1248. QCRow.insertCell(1).outerHTML = "<th>Dimensions</th>";
  1249. QCRow.insertCell(2).outerHTML = "<th>Original Size</th>";
  1250. QCRow.insertCell(3).outerHTML = "<th></th>";
  1251. QCRow.insertCell(4).outerHTML = "<th>New Size</th>";
  1252. QCRow.insertCell(5).outerHTML = "<th>Filename</th>";
  1253. QCRow.insertCell(6).outerHTML = "<th></th>";
  1254. QCListDiv.appendChild(QCTableWrapper);
  1255. QCTableWrapper.appendChild(QCTable);
  1256. //import/export buttons
  1257. var bottomQCL = document.createElement("div");
  1258. bottomQCL.style = "padding-top: 1em;";
  1259. var separator2 = document.createElement("span");
  1260. separator2.innerHTML = " | ";
  1261. var importQCList = document.createElement("a");
  1262. var exportQCList = document.createElement("a");
  1263. importQCList.innerHTML = "Import";
  1264. exportQCList.innerHTML = "Export";
  1265. importQCList.classList.add("menuBtns");
  1266. bottomQCL.innerHTML += '<input id="importQCLFile-input" type="file" accept=".json" style="display: none;" />'; //file-input
  1267. importQCList.onclick = function(){
  1268. document.getElementById('importQCLFile-input').click();
  1269. };
  1270. exportQCList.onclick = function(){ downloadObjectAsJson(getQCList(), "4chan Image Resizer v" + version + " Quick Convert List - " + Date.now()); }; //call file exporter
  1271. bottomQCL.appendChild(importQCList);
  1272. bottomQCL.appendChild(separator2);
  1273. bottomQCL.appendChild(exportQCList);
  1274. QCListDiv.appendChild(bottomQCL);
  1275. //import
  1276. document.getElementById('importQCLFile-input').addEventListener('change', function() {
  1277. var jsonFile = new FileReader();
  1278. jsonFile.onload = function() {
  1279. var originalQCL = getQCList();
  1280. var duplicateCount2 = 0;
  1281. var tempDuplicateCount2 = 0;
  1282. //parse raw text
  1283. var importedQCL = JSON.parse(jsonFile.result);
  1284. //check if array
  1285. if (Array.isArray(importedQCL)) {
  1286. for (let i = 0; i < importedQCL.length; i++) {
  1287. var line = importedQCL[i].split(':');
  1288. if (line.length != 7 || line[6].length != 32) {
  1289. if(DEBUG) console.log("[Error] Imported array does not match the required length (7) or contains an invalid MD5 hash.");
  1290. if(DEBUG) console.log(line);
  1291. alert("Error: Array length mismatch.\nThis file is either outdated or invalid.");
  1292. return;
  1293. }
  1294. else {
  1295. //check for duplicate MD5 hashes
  1296. for (let j = 0; j < originalQCL.length; j++) {
  1297. var originalLine2 = originalQCL[j].split(':');
  1298. if (line[6] == originalLine2[6]) {
  1299. tempDuplicateCount2++;
  1300. break;
  1301. }
  1302. }
  1303. //if not a dupe, push to the original array
  1304. if (tempDuplicateCount2 == 0) {
  1305. originalQCL.push(importedQCL[i]);
  1306. }
  1307. //count all duplicate entries
  1308. else {
  1309. duplicateCount2 += tempDuplicateCount2;
  1310. tempDuplicateCount2 = 0;
  1311. }
  1312. }
  1313. }
  1314. //add the final result to local storage
  1315. localStorage.setItem("downscale-qclist", JSON.stringify(originalQCL));
  1316. //rebuild list
  1317. document.getElementById("QCTable").tBodies.item(0).innerHTML = "";
  1318. printQCList();
  1319. var newEntries2 = importedQCL.length - duplicateCount2;
  1320. alert("Succesfully imported " + importedQCL.length + " entries.\nDuplicate entries skipped: " + duplicateCount2 + "\nNew entries added: " + newEntries2);
  1321. }
  1322. else {
  1323. alert("Error: Invalid data type.");
  1324. if(DEBUG) console.log("[Error] Imported data object is not an array.")
  1325. }
  1326. }
  1327. jsonFile.readAsText(this.files[0]);
  1328. });
  1329. //delete all QCL entries
  1330. var delAll = document.createElement("a");
  1331. var emptyArray = [];
  1332. delAll.innerHTML = "Delete All";
  1333. delAll.style = "float: right; margin-right: 1em;";
  1334. delAll.onclick = function(){
  1335. if (confirm(" WARNING!\nAre you sure you want to DELETE ALL entries from the \"Quick Convert List\"?")) {
  1336. localStorage.setItem("downscale-qclist", JSON.stringify(emptyArray));
  1337. document.getElementById("QCTable").tBodies.item(0).innerHTML = "";
  1338. }
  1339. };
  1340. bottomQCL.appendChild(delAll);
  1341. //INITIAL PRINT OF QUICK CONVERT LIST
  1342. printQCList();
  1343. //Help----------------------------------------------------------
  1344. var helpTitle = document.createElement("h3");
  1345. helpTitle.innerHTML = "About";
  1346. helpDiv.appendChild(helpTitle);
  1347. var rant = document.createElement("div");
  1348. rant.innerHTML = '<strong>4chan Image <span style="text-decoration-line: line-through;">Resizer</span></strong> <s>Downscaler</s> automatically downscales images based on custom presets. Originally developed to downscale anime/vidya screenshots "on the fly". Now supports image cropping!<br><br>' +
  1349. '<details><summary><strong>Presets</strong></summary><br>To automate image resizing, you have to create a preset by choosing an input image format and entering input and output dimensions (pixels). Then just add an image to a quick reply form. ' +
  1350. 'If it meets any of the created input requirements, the image will be automatically downscaled to your specified dimensions as a <strong>JPEG</strong>. ' +
  1351. '<br><br><strong>Note</strong> that output dimensions are constrained by input dimensions <strong>aspect ratio</strong>. ' +
  1352. '<br><strong>Also note</strong> that <strong>setting JPEG output quality to 1</strong> may result in filesizes larger than that of the original image, and should be considered as a placebo.</details>' +
  1353. //
  1354. '<br><details><summary><strong>Quick Convert</strong></summary><br>Allows you to quickly convert images (PNG/JPEG) to JPEG.' +
  1355. '<br>This is very useful when an image exceeds 4chan image size limit of <strong>4 MB</strong>.' +
  1356. '<br><br>It works well on super high resolution images (+3000px), sometimes drastically cutting the filesize without any noticeble quality loss.' +
  1357. ' However, <strong>it is not recommended to use it on grayscale PNG images</strong>, i.e. manga pages, because most of the time <strong>it will result in larger than original filesizes</strong>.' +
  1358. '<br>Once you are satisfied with the <strong>"Quick Convert"</strong> results, you can click <strong>"Remember"</strong> on the side menu to add the image MD5 hash to the <strong>"Quick Convert List"</strong>, which will always automatically convert the image for you in the future.' +
  1359. '<br><br>You can also use <kbd>Ctrl</kbd> + <kbd>Q</kbd> keyboard shortcut to perform <strong>"Quick Convert"</strong> faster. Press again to <strong>"Remember"</strong> the image.</details>' +
  1360. //
  1361. '<br><details><summary><strong>Import/Export</strong></summary><br>Allows you to seperatly backup both, "Presets" and "Quick Convert" lists as .json files.' +
  1362. '<br><strong>Import</strong> works by merging list entries instead of overwriting them, so you can export/import items between domains without any worry.</details>' +
  1363. //
  1364. '<br><details><summary><strong>Image Cropping</strong></summary><br>A basic image cropping tool that uses <a href="https://github.com/fengyuanchen/cropperjs" target="_blank">Cropper.js</a> libraby. ' +
  1365. 'You can change cropped image output format in the settings tab.<br><strong>PNG</strong> is lossless and preserves transperancy, but results in larger filesizes.<br><strong>JPEG</strong> is lossy, but results in smaller filesizes (the <strong>"JPEG quality"</strong> setting also applies here).' +
  1366. '<br><br><strong>Note</strong> that Cropper.js can sometimes cause strange, <a href="https://slow.pics/c/Zw8JapEG" target="_blank" title="comparison">spike-like artifacts</a> on images with sharp lines/edges(?), regardless of output format or quality setting. This can be "fixed" by enabling <strong>"Image Smoothing"</strong>. ' +
  1367. 'Side effects include smearing, blurriness and overall lower image quality.</details>' +
  1368. //
  1369. '<br><details><summary><strong>Cropper Controls</strong></summary>' +
  1370. '<br>While cropping:<ul><li>Double click <kbd>LMB</kbd> - switch between cropping and zooming.</li><li>Hold <kbd>Shift</kbd> while cropping - draw a rectangle.</li><li><kbd>MWheel</kbd> - zoom in/out.</li>' +
  1371. '<li><kbd>Backspace</kbd> - clear selection.</li><li><kbd>Enter</kbd> - confirm selection.</li><li><kbd>Esc</kbd> or <kbd>Delete</kbd> - close cropper.</li></ul>' +
  1372. 'After selection:<br><ul><li><kbd>Enter</kbd> or <kbd>LMB</kbd> - set image to QR form.</li><li><kbd>Backspace</kbd> or <kbd>RMB</kbd> - undo selection.</li><li><kbd>MMB</kbd> - close cropper.</li><li><kbd>Shift</kbd> + <kbd>RMB</kbd> - open context menu.</li></ul></details>' +
  1373. //
  1374. '<br><span style="font-weight:bold; color: red;">*NEW*</span><ul><li>Implemented <strong>Image Cropping</strong>.</li><li>Minor UI improvements and bug fixes.</li></ul>' +
  1375. '<br><br><div style="float: right;" >[ <a href="https://greasyfork.org/en/scripts/391758-4chan-image-resizer" target="_blank">version ' + version + '</a> ]</div>';
  1376. helpDiv.appendChild(rant);
  1377. }
  1378. //Only when QR form is open.
  1379. function appendSideMenu() {
  1380. //Arrow | sideMenuArrow----------------------------------------------------------
  1381. var arrow = document.createElement("a");
  1382. arrow.id = "sideMenuArrow";
  1383. arrow.title = "Side Menu";
  1384. arrow.style.cursor = "pointer";
  1385. arrow.innerHTML = "&#9664;";
  1386. var arrowRef = document.getElementById("autohide");
  1387. arrowRef.parentNode.insertAdjacentElement("beforebegin", arrow);
  1388. arrow.onclick = function(){ sideMenu.classList.toggle("downscale-menu-on"); };
  1389. //Side Menu | sideMenu----------------------------------------------------------
  1390. var sideMenu = document.createElement("div");
  1391. sideMenu.id = "sideMenu";
  1392. sideMenu.classList.add("dialog");
  1393. var sideMenuRef = document.getElementById("qr");
  1394. sideMenuRef.insertAdjacentElement("afterbegin", sideMenu);
  1395. //Close side menu dialog by clicking anywhere but here:
  1396. window.addEventListener('click', function(event) {
  1397. var getSideMenu = document.getElementById("sideMenu");
  1398. if (!event.target.matches('#sideMenuArrow') &&
  1399. !event.target.matches('#sideMenu') &&
  1400. !event.target.matches('#imgResize') &&
  1401. !event.target.matches('#quickConvert') &&
  1402. !event.target.matches('#imgResizeLabel')) {
  1403. if (getSideMenu.classList.contains('downscale-menu-on')) getSideMenu.classList.remove('downscale-menu-on');
  1404. }
  1405. });
  1406. }
  1407. appendSettings();
  1408. //*************************************************************************************//
  1409. //END OF MENUs //
  1410. //*************************************************************************************//
  1411. //Saves image details to local storage
  1412. function saveImgMD5 (img, file, imgMD5, newSize) {
  1413. removeRemOption();
  1414. var QCList = getQCList();
  1415. //"file/jpeg" -> "JPEG"
  1416. var filetype = file.type.split("/").pop().toUpperCase();
  1417. //remove filetype
  1418. var filename = file.name.split(".").slice(0,-1).join(".");
  1419. //replace seperators
  1420. filename = filename.replace(/:/g,"_");
  1421. var orig_filesize = formatBytes(file.size);
  1422. var new_filesize = formatBytes(newSize);
  1423. //QCList Array [0] - Filetype, [1] - Image Width, [2] - Image Height, [3] - Original Filesize, [4] - New Filesize, [5] - Filename, [6] - Image Base64 MD5 Hash
  1424. var QCString = filetype + ":" + img.width + ":" + img.height + ":" + orig_filesize + ":" + new_filesize + ":" + filename + ":" + imgMD5;
  1425. QCList.push(QCString);
  1426. localStorage.setItem("downscale-qclist", JSON.stringify(QCList));
  1427. //Show notification
  1428. var info = file.name + '\nAdded to the "Quick Convert List"';
  1429. var msgDetail = {type: 'info', content: info, lifetime: 5};
  1430. var msgEvent = new CustomEvent('CreateNotification', {bubbles: true, detail: msgDetail});
  1431. document.dispatchEvent(msgEvent);
  1432. //rebuild list
  1433. document.getElementById("QCTable").tBodies.item(0).innerHTML = "";
  1434. printQCList();
  1435. }
  1436. //Removes these Side Menu options
  1437. function removeQCOption() {
  1438. var checkQC = document.getElementById("qcDiv");
  1439. if (checkQC) checkQC.remove();
  1440. }
  1441. function removeRemOption() {
  1442. var checkRem = document.getElementById("remDiv");
  1443. if (checkRem) checkRem.remove();
  1444. }
  1445. function removeCropOption() {
  1446. var checkCrop = document.getElementById("cropDiv");
  1447. if (checkCrop) checkCrop.remove();
  1448. }
  1449. function removePreviewOption() {
  1450. var checkPreview = document.getElementById("previewImg");
  1451. if (checkPreview) checkPreview.remove();
  1452. }
  1453. function removeHR() {
  1454. var checkHR = document.getElementById("sm-hr");
  1455. if (checkHR) checkHR.remove();
  1456. }
  1457. //Get border color for <hr> hack
  1458. function getHRColor () {
  1459. var sample = document.getElementById("imgResizeMenu");
  1460. return window.getComputedStyle(sample, null).getPropertyValue("border-bottom-color");
  1461. }
  1462. //Show/hide sidemenu <hr>
  1463. function appendHR() {
  1464. var hr = document.createElement("hr");
  1465. hr.id = "sm-hr";
  1466. hr.style.borderColor = getHRColor();
  1467. document.getElementById("sideMenu").appendChild(hr);
  1468. }
  1469. //Image viewer
  1470. function showImage(img, size, width, height, filename) {
  1471. var overlay = document.createElement("div");
  1472. overlay.id = "pvOverlay";
  1473. //-----------------------------------------------
  1474. var pvHeader = document.createElement("div");
  1475. pvHeader.id = "pvHeader";
  1476. pvHeader.className = "dialog";
  1477. //opacity hack
  1478. pvHeader.classList.add("pvOpct");
  1479. pvHeader.innerHTML = filename + "<br>(" + formatBytes(size)+ ", " + Math.round(width) + "x" + Math.round(height) + ")";
  1480. //-----------------------------------------------
  1481. var closePv = document.createElement("a");
  1482. closePv.className = "close fa fa-times";
  1483. closePv.style = "float: right; cursor: pointer; margin-right: 20px; margin-top: -9px; transform: scale(1.5);";
  1484. closePv.title = "Close";
  1485. closePv.onclick = function(){ overlay.remove(); };
  1486. //-----------------------------------------------
  1487. var pvImg = document.createElement("img");
  1488. pvImg.id = "pvImg";
  1489. pvImg.classList.add("centerImg");
  1490. pvImg.title = "Click to close";
  1491. pvImg.src = img;
  1492. pvImg.onclick = function(){ overlay.remove(); };
  1493. //-----------------------------------------------
  1494. document.body.appendChild(overlay);
  1495. //pvHeader.appendChild(closePv);
  1496. overlay.appendChild(pvImg);
  1497. overlay.appendChild(pvHeader);
  1498. pvHeader.appendChild(closePv);
  1499. //opacity hack
  1500. setTimeout(function() { pvHeader.classList.toggle("pvOpct"); }, 2000);
  1501. }
  1502. //Converts dataURI to blob
  1503. function dataURItoBlob(dataURI) {
  1504. //convert base64/URLEncoded data component to raw binary data held in a string
  1505. var byteString;
  1506. if (dataURI.split(',')[0].indexOf('base64') >= 0) { byteString = atob(dataURI.split(',')[1]); }
  1507. else { byteString = unescape(dataURI.split(',')[1]); }
  1508. //separate out the mime component
  1509. var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
  1510. //write the bytes of the string to a typed array
  1511. var ia = new Uint8Array(byteString.length);
  1512. for (var i = 0; i < byteString.length; i++) {
  1513. ia[i] = byteString.charCodeAt(i);
  1514. }
  1515. return new Blob([ia], {
  1516. type: mimeString
  1517. });
  1518. }
  1519. //json file exporter
  1520. function downloadObjectAsJson(exportObj, exportName) {
  1521. var dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(exportObj));
  1522. var downloadAnchorNode = document.createElement('a');
  1523. downloadAnchorNode.setAttribute("href", dataStr);
  1524. downloadAnchorNode.setAttribute("download", exportName + ".json");
  1525. document.body.appendChild(downloadAnchorNode);
  1526. downloadAnchorNode.click();
  1527. downloadAnchorNode.remove();
  1528. }
  1529. //Prevent multiple event listeners
  1530. var scListenerExists = false;
  1531. //Quick Convert shortcut | Ctrl+Q
  1532. if (getSettings().shortcut && !scListenerExists) { document.addEventListener('keyup', qCShortcut); scListenerExists = true ; }
  1533. function qCShortcut(e) {
  1534. var convertBtn = document.getElementById("quickConvert");
  1535. var rememberBtn = document.getElementById("rememberMD5");
  1536. //if shortcut is enabled, simulate clicks
  1537. if (getSettings().shortcut) {
  1538. if (e.ctrlKey && e.keyCode == 81 && convertBtn) {
  1539. convertBtn.click();
  1540. }
  1541. else if (e.ctrlKey && e.keyCode == 81 && rememberBtn) {
  1542. rememberBtn.click();
  1543. }
  1544. }
  1545. }
  1546. //Fix filetype in filename
  1547. function constructFilename(mime, filename) {
  1548. //"file/jpeg" -> "jpeg"
  1549. var filetype = mime.split("/").pop();
  1550. //remove filetype from filename and add the correct one
  1551. var new_filename = filename.split(".").slice(0,-1).join(".").concat("." + filetype);
  1552. return new_filename;
  1553. }
  1554. //Bloat
  1555. function isDecimalNumber(e){var h=e.which?e.which:e.keyCode;return!(46!=h&&h>31&&(h<48||h>57));}
  1556. function isNumber(e){var i=(e=e||window.event).which?e.which:e.keyCode;return!(i>31&&(i<48||i>57));}
  1557. function formatBytes(a,b){if(0==a)return"0 Bytes";var c=1024,d=b||2,e=["Bytes","KB","MB","GB","TB","PB","EB","ZB","YB"],f=Math.floor(Math.log(a)/Math.log(c));return parseFloat((a/Math.pow(c,f)).toFixed(d))+" "+e[f];}