// ==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 = '→';
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> ' +
'' +
'<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> ' +
'  →   <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 = '→';
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>: ' +
'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>: ' +
'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>: ' +
'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>: ' +
'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 = "◀";
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];}