4chan Image Resizer

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

当前为 2020-12-29 提交的版本,查看 最新版本

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