4chan Image Resizer

Automatically downscale images based on custom presets. Features image cropping and WebP conversion. Requires 4chan X.

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