4chan Image Resizer

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

当前为 2020-10-18 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         4chan Image Resizer
// @namespace    https://greasyfork.org/en/users/393416
// @version      2.1
// @description  Automatically downscales images based on custom presets and more. Requires 4chan X.
// @author       greenronia
// @match        *://boards.4chan.org/*
// @match        *://boards.4channel.org/*
// @require      https://cdnjs.cloudflare.com/ajax/libs/spark-md5/3.0.0/spark-md5.js
// @grant none
// @icon         https://i.imgur.com/hQp5BTf.png
// ==/UserScript==
//
//Using SparkMD5 to generate image hashes - https://github.com/satazor/js-spark-md5
//*********************************//
//                                 //
//        "it just werks"          //
//                                 //
//*********************************//
//                                 //
//----------DEBUG MODE-------------//
var DEBUG = false;//console        //
//---------------------------------//
if(DEBUG) console.log("[ImageResizer] Initialized");
//CSS
var style = document.createElement("style");
style.innerHTML = '' +
    '.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' +
    '.settingsOverlay { background: rgba(0,0,0,0.8); display: none; height: 100%; left: 0; position: fixed; top: 0; width: 100%; z-index: 777; } \n' +
    '#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' +
    '#pvHeader { position: fixed; height: 35px; width: 100%; opacity: 0; -webkit-transition: opacity 0.5s ease-in-out;}\n' +
    '#pvHeader:hover { opacity: 0.8; -webkit-transition: none; }\n' +
    '.pvOpct { opacity: 0.7 !important; } \n' +
    '#imgResizeMenu { position: fixed; top: 20%; left: 35%; width: 35%; min-width: 670px; padding: 2em; overflow: hidden; z-index: 8;}\n' +
    '#imgResizeMenu h3 { text-align: center; }\n' +
    '#imgResizeMenu a { cursor: pointer; }\n' +
    '#imgResizeMenu label { text-decoration-line: underline; }\n' +
    '.settingsOverlay input[type=number] { -moz-appearance: textfield; text-align: right; }\n' +
    '.resizer-settings { padding-bottom: 5px }\n' +
    '#errMsg { color: red; text-align: center; }\n' +
    '#ruleTable { border-collapse: collapse; }\n' +
    '#ruleTable td, th { padding: 8px; text-align: left; border-bottom: 1pt solid; }\n' +
    '#QCTable { border-collapse: collapse; }\n' +
    '#QCTable td, th { padding: 8px; text-align: center; border-bottom: 1pt solid; }\n' +
    '#QCTable p { margin: auto; max-width: 100px; overflow: hidden; text-overflow: ellipsis; }\n' +
    '#inputContainer { text-align: center; padding-top: 30px; }\n' +
    '#inputContainer button { margin-top: 20px; }\n' +
    '.menuBtns { margin-left: 1em; }\n' +
    '#sideMenu { position: absolute; display: none; padding: 5px 0px 5px 0px; width: 101px; margin-left: -106px; margin-top: -2px;}\n' +
    '.sideMenuElement { background: inherit; display: block; cursor: pointer; padding: 2px 10px 2px 10px; text-align: left;}\n' +
    '.downscale-menu-off { display: none; }\n' +
    '.downscale-menu-on { display: block !important; }';
var styleRef = document.querySelector("script");
styleRef.parentNode.insertBefore(style, styleRef);
//Load settings
getSettings();
getPresets();
getQCList();
function getSettings() {
    if (JSON.parse(localStorage.getItem("downscale-settings"))) {
        var settings = JSON.parse(localStorage.getItem("downscale-settings"));
    }
    else {
        settings = { enabled:true, notify:true, convert:false, jpegQuality:0.92 };
    }
    return settings;
}
function getPresets() {
    if (JSON.parse(localStorage.getItem("downscale-presets"))) {
        var presets = JSON.parse(localStorage.getItem("downscale-presets"));
    }
    else {
        presets = [];
    }
    return presets;
}
function getQCList() {
    if (JSON.parse(localStorage.getItem("downscale-qclist"))) {
        var QCList = JSON.parse(localStorage.getItem("downscale-qclist"));
    }
    else {
        QCList = [];
    }
    return QCList;
}
//Checking if QuickReply dialog is open.
document.addEventListener('QRDialogCreation', function(listenForQRDC) {
    var checkBox = document.getElementById("imgResize");
    var sideMenu = document.getElementById("sideMenuArrow");
    //Checking if the "resize" check box and "side menu" already exist
    if (!sideMenu) {
        appendSideMenu();
    }
    if (!checkBox) {
        appendCheckBox();
    }
    //Listening for clicks on check box
    document.getElementById("imgResize").addEventListener("click", checkState);
    checkState(1);
    if(DEBUG) console.log("[QRFile] Listening...");
    //QRFile | Listening for QRFile, in response to: QRGetFile | Request File
    document.addEventListener('QRFile', function(GetFile) {
        if(DEBUG) console.log("[QRFile] File served: " + GetFile.detail);
        //Remove Remember option upon adding a (new) file.
        removeRemOption();
        const file = GetFile.detail;
        //Initialize an instance of a FileReader
        const reader = new FileReader();
        //Checking if the file is JPG or PNG
        if (file.type == "image/jpeg" || file.type == "image/png") {
            if(DEBUG) console.log("Acceptable File type: " + file.type);
            //Check if resizer already completed its task (to determine priority)
			var complete = false;
			var presets = getPresets();
            var QCList = getQCList();

			reader.onload = function(f) {
				var img = new Image();
				img.src = reader.result;

				img.onload = function() {
                    //Base64 MD5 hash of an image
                    var imgMD5 = SparkMD5.hash(img.src);
                    if(DEBUG) console.log("<FILTER START>");
                    if(DEBUG) if(getSettings().convert) console.log("[PNGConverter] Enabled"); else console.log("[PNGConverter] Disabled");
                    if(DEBUG) console.log("INPUT Dimensions: " + img.width + "x" + img.height);
                    if(DEBUG) console.log("INPUT File size: " + formatBytes(file.size));
                    //Add QC button
                    quickConvert(img, file, imgMD5);
                    //Remove/Add preview buton
                    removePreviewOption();
                    appendPreviewBtn(img.src, file.size, img.width, img.height, file.name);
                    //THE priority list
                    if (getQCList().length > 0) checkMD5(img, imgMD5);
					if (presets.length > 0 && !complete) checkPresets(img);
					if (getSettings().convert && !complete) checkPNG(img);
					return;
				}
				return;
			}

            function checkMD5(img, imgMD5) {
                if(DEBUG) console.log("[quickConvert] Checking for matching MD5: " + imgMD5);
                var filterCount = QCList.length;
                var matchFound = false;
                for (var i = 0; i < filterCount; i++) {
                    //unpack md5 hash
                    var filterMD5 = QCList[i].split(":").pop();
                    if (filterMD5 == imgMD5) {
                        if(DEBUG) console.log("[quickConvert] Match found.");
                        matchFound = true;
                        resizer(img.width, img.height, img);
                        break;
                    }
                }
                if(DEBUG) if (!matchFound)console.log("[quickConvert] No matching MD5 found.");
                return;
            }
			function checkPresets(img) {
				var matchCount = 0;
				var rule = [];
				var presetCount = presets.length;
				for (var i = 0; i < presetCount; i++) {
					//unpack rules
					rule[i] = presets[i].split(":");
					//check for matching file type
					if (rule[i][0] != 0) {
						switch (parseInt(rule[i][0])) {
							case 1:
								rule[i][0] = "image/png";
								break;
							case 2:
								rule[i][0] = "image/jpeg";
						}
						if (rule[i][0] != file.type) continue;
					}
					//check for matching dimensions
					if (rule[i][1] == img.width && rule[i][2] == img.height) {
						var MAX_WIDTH = parseInt(rule[i][3]);
						var MAX_HEIGHT = parseInt(rule[i][4]);
						matchCount++;
						if(DEBUG) console.log("Preset '" + i + "' matched: " + rule[i]);
						break;
					}
				}
				//failsafe
				if (matchCount == 0 || matchCount > 1) {
					if(DEBUG) console.log("Image didn't match any presets.\n------<END>------");
					return;
				}
                else {
                    resizer(MAX_WIDTH, MAX_HEIGHT, img);
                    return;
                }
			}
            //PNG -> JPEG
            function checkPNG(img) {
                if (file.type == "image/png") {
                    var MAX_WIDTH = img.width;
                    var MAX_HEIGHT = img.height;
                    if(DEBUG) console.log("[PNGConverter] Converting PNG to JPEG");
					resizer(MAX_WIDTH, MAX_HEIGHT, img);
                }
                else {
                    if(DEBUG) console.log("[PNGConverter] Image format isn't PNG.\n------<END>------");
                    return;
                }
            }
            //The main resize function
			function resizer(MAX_WIDTH, MAX_HEIGHT, img, imgMD5) {
                if(DEBUG && !imgMD5) console.log("<FILTER END>");
                removePreviewOption();
				var canvas = document.createElement("canvas");
                //Input dimensions
                var width = img.width;
                var height = img.height;
				//Calculating dimensions/aspect ratio
				if (width > height) {
					if (width > MAX_WIDTH) {
						height *= MAX_WIDTH / width;
						width = MAX_WIDTH;
					}
				} else {
					if (height > MAX_HEIGHT) {
						width *= MAX_HEIGHT / height;
						height = MAX_HEIGHT;
					}
				}
				// resize the canvas to the new dimensions
				canvas.width = width;
				canvas.height = height;
				// scale & draw the image onto the canvas
				var ctx = canvas.getContext("2d");
				ctx.drawImage(img, 0, 0, width, height);

				//Converts dataURI to blob
				function dataURItoBlob(dataURI) {
					//convert base64/URLEncoded data component to raw binary data held in a string
					var byteString;
					if (dataURI.split(',')[0].indexOf('base64') >= 0) { byteString = atob(dataURI.split(',')[1]); }
					else { byteString = unescape(dataURI.split(',')[1]); }
					//separate out the mime component
					var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
					//write the bytes of the string to a typed array
					var ia = new Uint8Array(byteString.length);
					for (var i = 0; i < byteString.length; i++) {
						ia[i] = byteString.charCodeAt(i);
					}
					return new Blob([ia], {
						type: mimeString
					});
				}
				//canvas to dataURL | JPEG quality (0-1)
                var dataURL;
                if (imgMD5) dataURL = canvas.toDataURL('image/jpeg', 92);
                else dataURL = canvas.toDataURL('image/jpeg', parseFloat(getSettings().jpegQuality));
				//dataURL to blob
				var blob = dataURItoBlob(dataURL);
				//Stop classObserver | prevent trigger loop
				classObserver.disconnect();
				if(DEBUG) console.log("[classObserver] Stopping...");
				setFile(blob, img, width, height, imgMD5);
                appendPreviewBtn(dataURL, blob.size, width, height, file.name);
			}
            //Set the new file to QR form
			function setFile(blob, img, width, height, imgMD5) {
				var detail = {
					file: blob,
					name: file.name
				};
				var event = new CustomEvent('QRSetFile', {
					bubbles: true,
					detail: detail
				});
				document.dispatchEvent(event);
                if (imgMD5) rememberQC(img, file, imgMD5, blob.size);
				if(DEBUG) console.log("[QRSetFile] File Sent");
				if(DEBUG) console.log("OUTPUT Dimesnions: " + Math.round(width) + "x" + Math.round(height));
				if(DEBUG) console.log("OUTPUT Filesize: " + formatBytes(blob.size));
				if(DEBUG) console.log("JPEG Quality: " + getSettings().jpegQuality);
				//Notification
				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) +")";
				if (getSettings().notify) {
					var msgDetail = {type: 'info', content: FSInfo, lifetime: 5};
					var msgEvent = new CustomEvent('CreateNotification', {bubbles: true, detail: msgDetail});
					document.dispatchEvent(msgEvent);
				}
                //Remove Quick Convert option after conversion
                removeQCOption();
				//Restart classObserver
				classObserver.observe(targetNode, observerOptions);
                //Preset priority
                complete = true;
				if(DEBUG) console.log("------<END>------\n[classObserver] Restarting...");
			}
            //Quick Convert (QC) image from Side Menu
            function quickConvert(img, file, imgMD5) {
                var existCheck = document.getElementById("quickConvert");
                //create QC button if it doesn't exist
                if (!existCheck) {
                    //Convert options container (future use)
                    var container = document.createElement("div");
                    container.id = "qcDiv";
                    //Convert button
                    var convert = document.createElement("a");
                    convert.id = "quickConvert";
                    convert.classList.add("sideMenuElement");
                    convert.classList.add("entry");
                    convert.innerHTML = "Quick Convert";
                    convert.title = "Convert image to JPEG";
                    //CSS on hover
                    convert.onmouseover = function(){this.classList.toggle("focused")};
                    convert.onmouseout = function(){this.classList.toggle("focused")};
                    var hr = document.createElement("hr");
                    hr.style.borderColor = getHRColor();
                    //Call resizer
                    convert.onclick = function(){
                        if(DEBUG) console.log("[quickConvert] Manually calling Resizer...");
                        resizer(img.width, img.height, img, imgMD5);
                    };
                    var parent = document.getElementById("sideMenu");
                    parent.appendChild(container);
                    container.appendChild(hr);
                    container.appendChild(convert);

                }
                //if QC button already exists
                else {
                    existCheck.onclick = function(){
                        if(DEBUG) console.log("[quickConvert] Manually calling Resizer...");
                        resizer(img.width, img.height, img, imgMD5);
                    };
                }
                return;
            }
            //Remember button
            function rememberQC (img, file, imgMD5, newSize) {
                var container = document.createElement("div");
                container.id = "remDiv";
                var remember = document.createElement("a");
                remember.id = "rememberMD5";
                remember.classList.add("sideMenuElement");
                remember.classList.add("entry");
                remember.innerHTML = "Remember";
                remember.style.fontWeight = "bold";
                remember.title = "Always convert this image."
                //CSS on hover
                remember.onmouseover = function(){this.classList.toggle("focused")};
                remember.onmouseout = function(){this.classList.toggle("focused")};
                var hr = document.createElement("hr");
                hr.style.borderColor = getHRColor();
                remember.onclick = function(){ saveImgMD5(img, file, imgMD5, newSize) };
                var parent = document.getElementById("sideMenu");
                parent.appendChild(container);
                container.appendChild(hr);
                container.appendChild(remember);
            }
            //Preview Image button
            function appendPreviewBtn(img, pvSize, pvWidth, pvHeight, pvName) {
                var existCheck = document.getElementById("previewImg");
                if (!existCheck) {
                    var preview = document.createElement("a");
                    preview.id = "previewImg";
                    preview.classList.add("sideMenuElement");
                    preview.classList.add("entry");
                    preview.innerHTML = "Preview Image";
                    //CSS on hover
                    preview.onmouseover = function(){this.classList.toggle("focused")};
                    preview.onmouseout = function(){this.classList.toggle("focused")};
                    preview.onclick = function(){ showImage(img, pvSize, pvWidth, pvHeight, pvName) };
                    var parent = document.getElementById("sideMenu");
                    parent.appendChild(preview);
                }
                else {
                    existCheck.onclick = function(){ showImage(img, pvSize, pvWidth, pvHeight, pvName) };
                }
                return;
            }
			//Read the file
            reader.readAsDataURL(file);
        } else {
            removeQCOption();
            if(DEBUG) console.log("[Error] Invalid FileType: " + file.type + "\n------<END>------");
        }
    }, false);
    //Observing if a file was uploaded or not | checking if div (with id: "file-n-submit") has class named: "has-file"
    function callback(mutationList, observer) {
        if (document.getElementById("file-n-submit").classList.contains("has-file") === true && checkState(2) === true) {
            if(DEBUG) console.log("------<START>------\n[classObserver] File detected")
            //QRGetFile | Request File
            if(DEBUG) console.log("[QRGetFile] Requesting file...");
            document.dispatchEvent(new CustomEvent('QRGetFile'));

        } else if (checkState(2) === false) {
            if(DEBUG) console.log("[classObserver] ImageResizer is disabled");
            return;
        }
        else {
            //Remove Side menu options upon removing a file.
            removeQCOption();
            removeRemOption();
            removePreviewOption();
            if(DEBUG) console.log("[classObserver] No file");
        }
    }
    //MutationObserver. Checks if div (with id "file-n-submit") has its class attribute changed
    const targetNode = document.getElementById("file-n-submit");
    var observerOptions = {
        attributes: true
    };
    var classObserver = new MutationObserver(callback);
    if(DEBUG) console.log("[classObserver] Starting...");
    classObserver.observe(targetNode, observerOptions);
}, false);
//*************************************************************************************//
//END OF THE MAIN PROCESS
//*************************************************************************************//
//Add a label with a check box for ImageResize + Setting button in Side Menu
function appendCheckBox() {
    var settingsButton = document.createElement("a");
    var label = document.createElement("label");
    var input = document.createElement("input");
    input.type = "checkbox";
    input.id = "imgResize";
    label.id = "imgResizeLabel";
    input.title = "Enable Image Resizer";
    input.style = "margin-left: 0";
    settingsButton.classList.add("sideMenuElement");
    settingsButton.classList.add("entry");
    label.classList.add("sideMenuElement");
    //CSS on hover
    label.classList.add("entry");
    var parent = document.getElementById("sideMenu");
    parent.appendChild(label);
    label.appendChild(input);
    label.title = "Enable Image Resizer";
    label.innerHTML += " Enabled";
    settingsButton.title = "Image Resizer Settings";
    settingsButton.innerHTML = "Settings";
    parent.appendChild(settingsButton);
    //CSS on hover
    label.onmouseover = function(){this.classList.toggle("focused")};
    label.onmouseout = function(){this.classList.toggle("focused")};
    settingsButton.onmouseover = function(){this.classList.toggle("focused")};
    settingsButton.onmouseout = function(){this.classList.toggle("focused")};
    //Open settings menu
    settingsButton.onclick = function(){ document.getElementById("imgResizeOverlay").style.display = "block" };
    //Checked by default
    document.getElementById("imgResize").checked = getSettings().enabled;
}
//Check box state
function checkState(caller) {
    var state = document.getElementById("imgResize").checked;
    if (state === true) {
        if (caller != 2) if(DEBUG) console.log("[ImageResizer] Enabled");
        return true;
    } else {
        if (caller != 2) if(DEBUG) console.log("[ImageResizer] Disabled");
        return false;
    }
}
//Clears error messages <p>
function clearErr() { document.getElementById("errMsg").innerHTML = ""; }
//Checks for any logic errors (upscaling)
function basicCheck(edit, rulePos) {
    var inWidth = parseInt(document.getElementById("inWidth").value);
    var inHeight = parseInt(document.getElementById("inHeight").value);
    var outWidth = parseInt(document.getElementById("outWidth").value);
    var outHeight = parseInt(document.getElementById("outHeight").value);
    var imgType = parseInt(document.getElementById("imgType").value);
    if (outWidth <= 0 || outHeight <= 0) { document.getElementById("errMsg").innerHTML = "Invalid output dimensions"; return}
    else if (inWidth < outWidth || inHeight < outHeight) { document.getElementById("errMsg").innerHTML = "Cannot upscale images"; return}
    else finalCheck(edit, imgType, inWidth, inHeight, outWidth, outHeight, rulePos);
    return;
}
//Checks for any rule overlaps
// ([0] - Image type, [1] - Input width, [2] - Input height, [3] - Output width, [4] - Output height)
function finalCheck(edit, imgType, inWidth, inHeight, outWidth, outHeight, rulePos) {
    var e = document.getElementById("imgType");
    var format = e.options[e.selectedIndex].text;
    var presetString = imgType + ":" + inWidth + ":" + inHeight + ":" + outWidth + ":" + outHeight;
    var presets = getPresets();
    if (presets.length > 0) {
        var rule = [];
        var presetCount = presets.length;
        for (var i = 0; i < presetCount; i++) {
            if (edit && i === rulePos) continue;
            rule[i] = presets[i].split(":");
            if (presetString == presets[i]) { document.getElementById("errMsg").innerHTML = "Exact preset already exists"; return }
            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 }
        }
    }
    //save preset
    clearErr();
    if (edit) presets[rulePos] = presetString;
    else presets.push(presetString);
    localStorage.setItem("downscale-presets", JSON.stringify(presets));
    //rebuild list
    document.getElementById("ruleTable").tBodies.item(0).innerHTML = "";
    printList();
    //hide / display
    document.getElementById("ruleInput").remove();
    document.getElementById("addRule").style.display = "inline";
    return;
}
//Check if possible to calculate output WIDTH
function aspectCheckH() {
    var inWidth = document.getElementById("inWidth").value;
    var inHeight = document.getElementById("inHeight").value;
    var outWidth = document.getElementById("outWidth").value;
    var outHeight = document.getElementById("outHeight").value;
    if (outHeight > 0) {
        if (parseInt(inHeight) >= parseInt(outHeight)) {
            calcAspect("width", inWidth, inHeight, outHeight);
            clearErr();
        }
        else {
            document.getElementById("errMsg").innerHTML = "Cannot upscale images";
        }
    }
}
//Check if possible to calculate output HEIGHT
function aspectCheckW() {
    var inWidth = document.getElementById("inWidth").value;
    var inHeight = document.getElementById("inHeight").value;
    var outWidth = document.getElementById("outWidth").value;
    var outHeight = document.getElementById("outHeight").value;
    if (outWidth > 0) {
        if (parseInt(inWidth) >= parseInt(outWidth)) {
            calcAspect("height", inWidth, inHeight, outWidth);
            clearErr();
        }
        else {
            document.getElementById("errMsg").innerHTML = "Cannot upscale images";
        }
    }
}
//Aspect ratio calculation (finds the other output dimension based on given exact input dimensions)
function calcAspect(dimension, w, h, output) {
    if (dimension == "width") {
        var width = output / h * w;
        document.getElementById("outWidth").value = Math.round(width);
    }
    if (dimension == "height") {
        var height = output / w * h;
        document.getElementById("outHeight").value = Math.round(height);
    }
}
//Populate Presets list
function printList() {
    var presets = getPresets();
    var list = document.getElementById("imgResizeList");
    var table = document.getElementById("ruleTable");
    if (presets.length > 0) {
        var rule = [];
        var presetCount = presets.length;
        for (let i = 0; i < presetCount; i++) {
            rule[i] = presets[i].split(":");
            switch (parseInt(rule[i][0])) {
                case 0:
                    rule[i][0] = "PNG/JPEG";
                    break;
                case 1:
                    rule[i][0] = "PNG";
                    break;
                case 2:
                    rule[i][0] = "JPEG";
            }
            let delRow = document.createElement("a");
            let editRow = document.createElement("a");
            delRow.innerHTML = "delete";
            editRow.innerHTML = "edit";
            //delete a rule and rebuild the list
            delRow.onclick = function() {
                if (document.getElementById("inputContainer")) document.getElementById("inputContainer").innerHTML = "";
                presets.splice(delRow.parentElement.parentElement.sectionRowIndex, 1);
                localStorage.setItem("downscale-presets", JSON.stringify(presets));
                table.tBodies.item(0).innerHTML = "";
                printList();
                clearErr();
                document.getElementById("addRule").style.display = "inline";
            };
            editRow.onclick = function() { inputUI(true, rule[i], i); clearErr(); };
            //Array contents: [0] - Image type, [1] - Input width, [2] - Input height, [3] - Output width, [4] - Output height
            var row = table.tBodies.item(0).insertRow(-1);
            row.insertCell(0).innerHTML = rule[i][0];
            row.insertCell(1).innerHTML = '[ ' + rule[i][1] + ' x ' + rule[i][2] + ' ]';
            row.insertCell(2).innerHTML = '&#8594;';
            row.insertCell(3).innerHTML = '[ ' + rule[i][3] + ' x ' + rule[i][4] + ' ]';
            row.insertCell(4).appendChild(editRow);
            row.insertCell(5).appendChild(delRow);
        }
    }
}
//Input field
function inputUI(edit, rule, rulePos) {
    if (document.getElementById("inputContainer")) document.getElementById("inputContainer").innerHTML = "";
    document.getElementById("addRule").style.display = "none";
    var inputDiv = document.getElementById("inputContainer");
    var input = document.createElement("div");
    var discardRuleBtn = document.createElement("button");
    discardRuleBtn.innerHTML = "Cancel";
    var saveRuleBtn = document.createElement("button");
    saveRuleBtn.innerHTML = "Save";
    input.id = "ruleInput";
    //Rules form
    input.innerHTML = '' +
        '' +
        '<select id="imgType" name="imgType" title="Input Format">' +
        '<option value="0">PNG/JPEG</option>' +
        '<option value="1">PNG</option>' +
        '<option value="2">JPEG</option>' +
        '</select>&ensp;' +
        '' +
        '<input type="number" id="inWidth" title="Input Width" size="2" min="0" value="0" onfocus="this.select();"></input> x ' +
        '' +
        '<input type="number" id="inHeight" title="Input Height" size="2" min="0" value="0" onfocus="this.select();"></input> ' +
        '&ensp; &#8594; &ensp; <input type="number" id="outWidth" title="Output Width" size="2" min="0" value="0" onfocus="this.select();"></input> x ' +
        '<input type="number" id="outHeight" title="Output Height" size="2" min="0" value="0" onfocus="this.select();"></input><br>';
    inputDiv.appendChild(input);
    var inWidth = document.getElementById("inWidth");
    var inHeight = document.getElementById("inHeight");
    var outWidth = document.getElementById("outWidth");
    var outHeight = document.getElementById("outHeight");
    if (edit) {
        switch (rule[0]) {
            case "PNG/JPEG":
                document.getElementById("imgType").selectedIndex = 0;
                break;
            case "PNG":
                document.getElementById("imgType").selectedIndex = 1;
                break;
            case "JPEG":
                document.getElementById("imgType").selectedIndex = 2;
        }
        inWidth.value = rule[1];
        inHeight.value = rule[2];
        outWidth.value = rule[3];
        outHeight.value = rule[4];
    }
    //Listen for user input on target dimension input fields to automatically calculate aspect ratio
    outWidth.addEventListener("input", aspectCheckW);
    outHeight.addEventListener("input", aspectCheckH);
    inWidth.onkeypress = function() { outHeight.value = 0; outWidth.value = 0; return isNumber(event); };
    inHeight.onkeypress = function() { outHeight.value = 0; outWidth.value = 0; return isNumber(event); };
    outWidth.onkeypress = function() { return isNumber(event); };
    outHeight.onkeypress = function() { return isNumber(event); };

    input.appendChild(saveRuleBtn);
    input.appendChild(discardRuleBtn);
    discardRuleBtn.onclick = function(){ document.getElementById(input.id).remove(); document.getElementById("addRule").style.display = "inline"; clearErr();};
    saveRuleBtn.onclick = function() { if (edit) basicCheck(true, rulePos); else basicCheck(false); };
}
//Populate Quick Convert List table
function printQCList() {
    var QCList = getQCList();
    var list = document.getElementById("QCList");
    var table = document.getElementById("QCTable");
    var filterCount = QCList.length;
    if (filterCount > 0) {
        var QCFilter = [];
        for (let i = 0; i < filterCount; i++) {
            QCFilter[i] = QCList[i].split(":");
            let delRow = document.createElement("a");
            delRow.innerHTML = "delete";
            delRow.onclick = function() {
                QCList.splice(delRow.parentElement.parentElement.sectionRowIndex, 1);
                localStorage.setItem("downscale-qclist", JSON.stringify(QCList));
                table.tBodies.item(0).innerHTML = "";
                printQCList();
            };
            //QCList Array: [0] - Filetype, [1] - Image Width, [2] - Image Height, [3] - Original Filesize, [4] - New Filesize, [5] - Filename, [6] - Image Base64 MD5 Hash
            var row = table.tBodies.item(0).insertRow(-1);
            row.insertCell(0).innerHTML = QCFilter[i][0];
            row.insertCell(1).innerHTML = '[ ' + QCFilter[i][1] + ' x ' + QCFilter[i][2] + ' ]';
            row.insertCell(2).innerHTML = QCFilter[i][3];
            row.insertCell(3).innerHTML = '&#8594;';
            row.insertCell(4).innerHTML = QCFilter[i][4];
            row.insertCell(5).innerHTML = '<p title = "' + QCFilter[i][5] +'">' + QCFilter[i][5] + '</p>';
            row.insertCell(6).innerHTML = '<p title = "' + QCFilter[i][6] +'">' + QCFilter[i][6] + '</p>';
            row.insertCell(7).appendChild(delRow);
        }
    }
}
//*************************************************************************************//
//                         /!\ very shitty menu ahead /!\                              //
//  I'm too lazy to fix this mess                                                      //
//*************************************************************************************//
function appendSettings() {
    //Button--------------------------------------------------------
    var span = document.createElement("span");
    var button = document.createElement("a");
    button.id = "imgResizeSettings";
    button.className += "fa fa-cog";
    button.style = "cursor: pointer;";
    button.title = "Image Resizer Settings";
    var ref = document.getElementById('shortcut-settings');
    ref.insertBefore(span, parent.nextSibling);
    span.appendChild(button);
    //Overlay | imgResizeOverlay------------------------------------
    var overlay = document.createElement("div");
    overlay.id = "imgResizeOverlay";
    overlay.classList.add("settingsOverlay");
    document.body.appendChild(overlay);
    //Settings menu links | imgResizeMenu---------------------------
    var menu = document.createElement("div");
    menu.id = "imgResizeMenu";
    menu.classList.add("dialog");
    overlay.appendChild(menu);
    var close = document.createElement("a");
    close.className += "close fa fa-times";
    close.style = "float: right;";
    close.title = "Close";
    menu.insertAdjacentElement('afterbegin', close);
    //Settings
    var settingsBtn = document.createElement("a");
    settingsBtn.innerHTML += "Settings";
    settingsBtn.classList.add("menuBtns");
    settingsBtn.style = "font-weight: bold;";
    settingsBtn.onclick = function() {
        settingsDiv.className = "downscale-menu-on";
        presetsDiv.className = "downscale-menu-off";
        QCListDiv.className = "downscale-menu-off";
        helpDiv.className = "downscale-menu-off";
        settingsBtn.style = "font-weight: bold;";
        presetsBtn.style = "";
        QCListBtn.style = "";
        helpBtn.style = "";
    };
    menu.appendChild(settingsBtn);
    //Presets
    var presetsBtn = document.createElement("a");
    presetsBtn.innerHTML += "Presets";
    presetsBtn.classList.add("menuBtns");
    presetsBtn.onclick = function() {
        settingsDiv.className = "downscale-menu-off";
        presetsDiv.className = "downscale-menu-on";
        QCListDiv.className = "downscale-menu-off";
        helpDiv.className = "downscale-menu-off";
        settingsBtn.style = "";
        presetsBtn.style = "font-weight: bold;";
        QCListBtn.style = "";
        helpBtn.style = "";
    };
    menu.appendChild(presetsBtn);
    //Quick Convert List
    var QCListBtn = document.createElement("a");
    QCListBtn.innerHTML += "Quick Convert";
    QCListBtn.classList.add("menuBtns");
    QCListBtn.onclick = function() {
        settingsDiv.className = "downscale-menu-off";
        presetsDiv.className = "downscale-menu-off";
        QCListDiv.className = "downscale-menu-on";
        helpDiv.className = "downscale-menu-off";
        settingsBtn.style = "";
        presetsBtn.style = "";
        QCListBtn.style = "font-weight: bold;";
        helpBtn.style = "";
    };
    menu.appendChild(QCListBtn);
    //Help
    var helpBtn = document.createElement("a");
    helpBtn.innerHTML += "About";
    helpBtn.classList.add("menuBtns");
    helpBtn.onclick = function() {
        settingsDiv.className = "downscale-menu-off";
        presetsDiv.className = "downscale-menu-off";
        QCListDiv.className = "downscale-menu-off";
        helpDiv.className = "downscale-menu-on";
        settingsBtn.style = "";
        presetsBtn.style = "";
        QCListBtn.style = "";
        helpBtn.style = "font-weight: bold;";
    };
    menu.appendChild(helpBtn);
    var hr = document.createElement("hr");
    hr.style.borderColor = getHRColor();
    menu.appendChild(hr);
    //Content divs| imgResizeContent------------------------------------
    var content = document.createElement("div");
    content.id = "imgResizeContent";
    menu.appendChild(content);
    content.innerHTML = "";
    var errMsg = document.createElement("p");
    errMsg.id = "errMsg";
    //Settings
    var settingsDiv = document.createElement("div");
    settingsDiv.id = "settingsDiv";
    settingsDiv.classList.add("downscale-menu-on");
    content.appendChild(settingsDiv);
    //Presets
    var presetsDiv = document.createElement("div");
    presetsDiv.id = "presetsDiv";
    presetsDiv.classList.add("downscale-menu-off");
    presetsDiv.style.textAlign = "center";
    content.appendChild(presetsDiv);
    //Quick Convert List
    var QCListDiv = document.createElement("div");
    QCListDiv.id = "QCListDiv";
    QCListDiv.classList.add("downscale-menu-off");
    content.appendChild(QCListDiv);
    //Help
    var helpDiv = document.createElement("div");
    helpDiv.id = "heplDiv";
    helpDiv.classList.add("downscale-menu-off");
    content.appendChild(helpDiv);
    //Enable Resizer------------------------------------------------
    var title = document.createElement("h3");
    title.innerHTML = "Image Resizer Settings";
    settingsDiv.appendChild(title);
    var enableDiv = document.createElement("div");
    enableDiv.classList.add("resizer-settings");
    enableDiv.innerHTML = '' +
        '<input type="checkbox" id="enableSet" title="" size="1"></input>' +
        '<label for="enableSet">Enable Resizer</label>:&ensp;' +
        'Enable / disable 4chan Image Resizer.';
    settingsDiv.appendChild(enableDiv);
    var enableSet = document.getElementById('enableSet');
    enableSet.checked = getSettings().enabled;
    enableSet.oninput = function() {
        var settings = getSettings();
        settings.enabled = enableSet.checked;
        document.getElementById("imgResize").checked = enableSet.checked;
        localStorage.setItem("downscale-settings", JSON.stringify(settings));
    };
    //Display notifications-----------------------------------------
    var notifySetDiv = document.createElement("div");
    notifySetDiv.classList.add("resizer-settings");
    notifySetDiv.innerHTML = '' +
        '<input type="checkbox" id="displaySet" title="" size="1"></input>' +
        '<label for="displaySet">Display Notifications</label>:&ensp;' +
        'Displays a notification when an image is downscaled.';
    settingsDiv.appendChild(notifySetDiv);
    var notifySet = document.getElementById('displaySet');
    notifySet.checked = getSettings().notify;
    notifySet.oninput = function() {
        var settings = getSettings();
        settings.notify = notifySet.checked;
        localStorage.setItem("downscale-settings", JSON.stringify(settings));
    };
    //Convert all PNGs to JPEGs-------------------------------------
    var convertSetDiv = document.createElement("div");
    convertSetDiv.classList.add("resizer-settings");
    convertSetDiv.innerHTML = '' +
        '<input type="checkbox" id="convertSet" title="" size="1"></input>' +
        '<label for="convertSet">Convert All PNGs</label>:&ensp;' +
        'Automatically converts all added PNGs to JPEGs. Presets apply as normal.';
    settingsDiv.appendChild(convertSetDiv);
    var convertSet = document.getElementById('convertSet');
    convertSet.checked = getSettings().convert;
    convertSet.oninput = function() {
        var settings = getSettings();
        settings.convert = convertSet.checked;
        localStorage.setItem("downscale-settings", JSON.stringify(settings));
    };
    //Set JPEG quality----------------------------------------------
    //RegExp ^(0(\.\d{1,2})?|1(\.0+)?)$
    //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)
    //e.g. 0.92 true, 1.92 false
    var qualitySetDiv = document.createElement("div");
    qualitySetDiv.classList.add("resizer-settings");
    qualitySetDiv.innerHTML = '' +
        '<input type="text" id="imgQuality" title="JPEG Quality" size="1"></input>' +
        '<label for="imgQuality">JPEG Quality</label>:&ensp;' +
        'A number between 0 and 1 indicating the output image quality. Recommended 0.92.';
    settingsDiv.appendChild(qualitySetDiv);
    var inputField = document.getElementById('imgQuality');
    inputField.value = getSettings().jpegQuality;
    inputField.onkeypress = function() { return isDecimalNumber(event); };
    //Check input field validity
    inputField.oninput = function() {
        var inputField = document.getElementById('imgQuality');
        var r = new RegExp(/^(0(\.\d{1,2})?|1(\.0+)?)$/);
        if(r.test(document.getElementById('imgQuality').value)) {
            inputField.setCustomValidity("");
            var settings = getSettings();
            settings.jpegQuality = inputField.value;
            localStorage.setItem("downscale-settings", JSON.stringify(settings));
        }
        else inputField.setCustomValidity("Set the value between 1 and 0 up to 2 numbers after the decimal point.");
    };
    //Preset table | ruleTable----------------------------------------
    var tableWrapper = document.createElement("div");
    tableWrapper.style.overflowY = "auto";
    tableWrapper.style.maxHeight = "200px";
    var table = document.createElement("table");
    var thead = document.createElement("thead");
    var tbody = document.createElement("tbody");
    var presetsTitle = document.createElement("h3");
    presetsTitle.innerHTML = "Presets";
    presetsDiv.appendChild(presetsTitle);
    table.appendChild(thead);
    table.appendChild(tbody);
    table.id = "ruleTable";
    var row = thead.insertRow(0);
    row.insertCell(0).outerHTML = "<th>Format</th>";
    row.insertCell(1).outerHTML = "<th>Input</th>";
    row.insertCell(2).outerHTML = "<th></th>";
    row.insertCell(3).outerHTML = "<th>Output</th>";
    row.insertCell(4).outerHTML = "<th></th>";
    row.insertCell(5).outerHTML = "<th></th>";
    presetsDiv.appendChild(tableWrapper);
    tableWrapper.appendChild(table);
    //Input container | inputContainer------------------------------
    var inputDiv = document.createElement("div");
    inputDiv.id = "inputContainer";
    presetsDiv.appendChild(inputDiv);
    var addRuleBtn = document.createElement("button");
    addRuleBtn.id = "addRule";
    addRuleBtn.innerHTML = "New Preset";
    printList();
    presetsDiv.appendChild(addRuleBtn);
    presetsDiv.appendChild(errMsg);
    button.onclick = function(){ overlay.style.display = "block"; };
    close.onclick = function(){ overlay.style.display = "none"; };
    window.addEventListener('click', function(closeSettingsMenu) {
        if (closeSettingsMenu.target == overlay) overlay.style.display = "none";
    });
    addRuleBtn.onclick = function(){ inputUI(false); };
    //Quick Convert table | QCTable----------------------------------
    var QCTableWrapper = document.createElement("div");
    QCTableWrapper.style.overflowY = "auto";
    QCTableWrapper.style.maxHeight = "210px";
    var QCTable = document.createElement("table");
    var QCThead = document.createElement("thead");
    var QCTbody = document.createElement("tbody");
    var QCTitle = document.createElement("h3");
    QCTitle.innerHTML = "Quick Convert List";
    QCListDiv.appendChild(QCTitle);
    QCListDiv.innerHTML += "<p style='text-align: center;'>Images on this list will be automatically converted to JPEG with a quality setting of 92.</p>";
    QCTable.appendChild(QCThead);
    QCTable.appendChild(QCTbody);
    QCTable.id = "QCTable";
    var QCRow = QCThead.insertRow(0);
    QCRow.insertCell(0).outerHTML = "<th>Format</th>";
    QCRow.insertCell(1).outerHTML = "<th>Dimensions</th>";
    QCRow.insertCell(2).outerHTML = "<th>Original Size</th>";
    QCRow.insertCell(3).outerHTML = "<th></th>";
    QCRow.insertCell(4).outerHTML = "<th>New Size</th>";
    QCRow.insertCell(5).outerHTML = "<th>Filename</th>";
    QCRow.insertCell(6).outerHTML = "<th>MD5 Hash</th>";
    QCRow.insertCell(7).outerHTML = "<th></th>";
    QCListDiv.appendChild(QCTableWrapper);
    QCTableWrapper.appendChild(QCTable);
    printQCList();
    //Help----------------------------------------------------------
    var helpTitle = document.createElement("h3");
    helpTitle.innerHTML = "About";
    helpDiv.appendChild(helpTitle);
    var rant = document.createElement("p");
    rant.innerHTML = '<span style="font-weight:bold;">4chan Image <span style="text-decoration-line: line-through;">Resizer</span></span> <s>can\'t upscale</s> automatically downscales images based on custom presets. Originally developed to downscale anime/vidya screenshots "on the fly".<br><br>' +
        '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 (drag & drop) an image to a quick reply form. ' +
        '<br>If it meets any of the presets input requirements, the image will be automatically downscaled to specified dimensions as a <span style="font-weight:bold;">JPEG</span>. ' +
        '<br><br><span style="font-weight:bold;">Note</span> that output dimensions are constrained by input dimensions <span style="font-weight:bold;">aspect ratio</span>. ' +
        '<br><span style="font-weight:bold;">Also note</span> that <span style="font-weight:bold;">setting JPEG output quality to 1</span> may result in filesizes larger than that of the original image, and should be considered as a placebo.' +
        '<br><br><span style="font-weight:bold; color: red;">NEW</span><br><span style="font-weight:bold;"> "Quick Convert"</span> allows you to quickly convert images (PNG/JPEG) to JPEG at a quality of 92 <s>for now</s>.' +
        '<br>This is very useful when an image exceeds 4chan image size limit of <span style="font-weight:bold;">4 MB</span> (higher on some other boards) and you do not want to, or cannot be bothered to lower the resolution manually.' +
        '<br>It works well on super high resolution images (+3000px), sometimes drastically cutting the filesize without any noticeble quality loss.' +
        ' However, <span style="font-weight:bold;">it is not recommended to use it on grayscale PNG images</span>, i.e. manga pages, because most of the time <span style="font-weight:bold;">it will result in larger than original filesizes</span>.' +
        '<br>Once you are satisfied with the "Quick Convert" results, you can click "Remember" on the "Side Menu" to add the image MD5 hash <s>not the actual image MD5 hash, the Image->Base64->MD5 hash</s> to "Quick Convert List", which allows you to always automatically convert this image before posting.' +
        '<br><br> Also added a simple <span style="font-weight:bold;">"Preview Image"</span> feature for JPEGs/PNGs. Click on the image to close it.' +
        '<br><a style="float: right;" href="https://greasyfork.org/en/scripts/391758-4chan-image-resizer" target="_blank">[version 2.1]</a>';
    helpDiv.appendChild(rant);
}
//Only when QR form is open.
function appendSideMenu() {
    //Arrow | sideMenuArrow----------------------------------------------------------
    var arrow = document.createElement("a");
    arrow.id = "sideMenuArrow";
    arrow.title = "Image Settings";
    arrow.style.cursor = "pointer";
    arrow.innerHTML = "&#9664;";
    var arrowRef = document.getElementById("autohide");
    arrowRef.parentNode.insertAdjacentElement("beforebegin", arrow);
    arrow.onclick = function(){ sideMenu.classList.toggle("downscale-menu-on"); };
    //Side Menu | sideMenu----------------------------------------------------------
    var sideMenu = document.createElement("div");
    sideMenu.id = "sideMenu";
    sideMenu.classList.add("dialog");
    var sideMenuRef = document.getElementById("qr");
    sideMenuRef.insertAdjacentElement("afterbegin", sideMenu);
    //Close side menu dialog by clicking anywhere but here:
    window.addEventListener('click', function(event) {
        var getSideMenu = document.getElementById("sideMenu");
        if (!event.target.matches('#sideMenuArrow') &&
            !event.target.matches('#sideMenu') &&
            !event.target.matches('#imgResize') &&
            !event.target.matches('#quickConvert') &&
            !event.target.matches('#imgResizeLabel')) {
            if (getSideMenu.classList.contains('downscale-menu-on')) getSideMenu.classList.remove('downscale-menu-on');
        }
    });
}
//*************************************************************************************//
//END OF MENUs                                                                         //
//*************************************************************************************//
//Saves image details to local storage
function saveImgMD5 (img, file, imgMD5, newSize) {
    removeRemOption();
    var QCList = getQCList();
    //"file/jpeg" -> "JPEG"
    var filetype = file.type.split("/").pop().toUpperCase();
    //remove filetype
    var filename = file.name.split(".").slice(0,-1).join(".");
    var orig_filesize = formatBytes(file.size);
    var new_filesize = formatBytes(newSize);
    //QCList Array [0] - Filetype, [1] - Image Width, [2] - Image Height, [3] - Original Filesize, [4] - New Filesize, [5] - Filename, [6] - Image Base64 MD5 Hash
    var QCString = filetype + ":" + img.width + ":" + img.height + ":" + orig_filesize + ":" + new_filesize + ":" + filename + ":" + imgMD5;
    QCList.push(QCString);
    localStorage.setItem("downscale-qclist", JSON.stringify(QCList));
    //Show notification
    var info = 'Added to the "Quick Convert List"';
    var msgDetail = {type: 'info', content: info, lifetime: 5};
    var msgEvent = new CustomEvent('CreateNotification', {bubbles: true, detail: msgDetail});
    document.dispatchEvent(msgEvent);
    //Print list
    document.getElementById("QCTable").tBodies.item(0).innerHTML = "";
    printQCList();
}
//Removes these Side Menu options
function removeQCOption() {
    var checkQC = document.getElementById("qcDiv");
    if (checkQC) checkQC.remove();
}
function removeRemOption() {
    var checkRem = document.getElementById("remDiv");
    if (checkRem) checkRem.remove();
}
function removePreviewOption() {
    var checkPreview = document.getElementById("previewImg");
    if (checkPreview) checkPreview.remove();
}
//Get border color for <hr> hack
function getHRColor () {
    var sample = document.getElementById("imgResizeMenu");
    return window.getComputedStyle(sample, null).getPropertyValue("border-bottom-color");
}
function showImage(img, size, width, height, filename) {
    var overlay = document.createElement("div");
    overlay.id = "pvOverlay";
    //-----------------------------------------------
    var pvHeader = document.createElement("div");
    pvHeader.id = "pvHeader";
    pvHeader.className = "dialog";
    //opacity hack
    pvHeader.classList.add("pvOpct");
    pvHeader.innerHTML = filename + "<br>(" + formatBytes(size)+ ", " + Math.round(width) + "x" + Math.round(height) + ")";
    //-----------------------------------------------
    var closePv = document.createElement("a");
    closePv.className = "close fa fa-times";
    closePv.style = "float: right;";
    closePv.onclick = function(){ overlay.remove(); };
    //-----------------------------------------------
    var pvImg = document.createElement("img");
    pvImg.id = "pvImg";
    pvImg.classList.add("centerImg");
    pvImg.title = "Click to close";
    pvImg.src = img;
    pvImg.onclick = function(){ overlay.remove(); };
    //-----------------------------------------------
    document.body.appendChild(overlay);
    //overlay.appendChild(closePv);
    overlay.appendChild(pvImg);
    overlay.appendChild(pvHeader);
    //opacity hack
    setTimeout(function() { pvHeader.classList.toggle("pvOpct"); }, 2000);
}
appendSettings();
//Bloat
function isDecimalNumber(e){var h=e.which?e.which:e.keyCode;return!(46!=h&&h>31&&(h<48||h>57));}
function isNumber(e){var i=(e=e||window.event).which?e.which:e.keyCode;return!(i>31&&(i<48||i>57));}
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];}