Last active
July 25, 2022 13:29
-
-
Save funmaker/00793ba842b12538d95e0fc3d8c626cd to your computer and use it in GitHub Desktop.
Resize images before post to meet 4chan's limits
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name 4chan image resizer | |
// @version 0.6 | |
// @description Resize images before post to meet 4chan's limits | |
// @author Fun Maker | |
// @namespace https://gist.github.com/00793ba842b12538d95e0fc3d8c626cd | |
// @match http*://boards.4chan.org/* | |
// @match http*://boards.4channel.org/* | |
// @icon https://4chan.org/favicon.ico | |
// @updateURL https://gist.github.com/funmaker/00793ba842b12538d95e0fc3d8c626cd/raw/4chan_image_resizer.user.js | |
// @downloadURL https://gist.github.com/funmaker/00793ba842b12538d95e0fc3d8c626cd/raw/4chan_image_resizer.user.js | |
// @grant none | |
// ==/UserScript== | |
const QUALITY = 95; // JPEG quality | |
const MAX_DIM = 10000; // Maximum image resolution, constant across the page | |
const SIZE_MARGIN = 0.1; // How close final image size has to be to maximum file size, (smaller = more tries) | |
const MAX_TRIES = 30; // Infinity loop prevention | |
const PRESERVE_MIME = false; // Preserve format if possible (unless browser fallbacks to png or whatever) | |
const PRESERVE_ALPHA = true; // If converting/scaling, use png if the image uses alpha channel, otherwise jpeg. May cause some lag. Ignored if PRESERVE_MIME = true | |
const CONVERT_MIME = [ | |
"image/webp", | |
]; | |
(function() { | |
'use strict'; | |
let onInterject = null; | |
// Interject Quick Reply submit | |
const oldSubmit = window.QR.submit; | |
window.QR.submit = (...args) => { | |
const oldXHR = window.XMLHttpRequest; | |
window.XMLHttpRequest = class FakeXMLHttpRequest extends window.XMLHttpRequest { | |
async send(data) { | |
try { | |
if(onInterject) await onInterject(this, data); | |
} catch(e) { | |
if(this.aborted) return; | |
else throw e; | |
} | |
if(this.aborted) return; | |
super.send(data); | |
} | |
abort(...args) { | |
this.aborted = true; | |
super.abort(...args); | |
} | |
}; | |
try { | |
oldSubmit(...args); | |
} finally { | |
window.XMLHttpRequest = oldXHR; | |
} | |
} | |
// Watch quick reply form for file input and submit button | |
function registerForm(node) { | |
let fileInput = node.querySelector('input[name="upfile"]'); | |
const submitButton = node.querySelector('input[type="submit"]'); | |
submitButton.style.width = "auto"; | |
submitButton.style.minWidth = "75px"; | |
const updateButton = (text, callback = null) => { | |
submitButton.value = text; | |
onInterject = callback; | |
} | |
const image = new Image(); | |
let mime = null; | |
// Resize and replace image before submit | |
async function resizeSubmit(xhr, formData) { | |
let max = 1, | |
min = 0; | |
if(max * image.width > MAX_DIM) { | |
max = MAX_DIM / image.width; | |
} | |
if(max * image.height > MAX_DIM) { | |
max = MAX_DIM / image.height; | |
} | |
const canvas = document.createElement('canvas'); | |
const ctx = canvas.getContext('2d'); | |
if(PRESERVE_MIME && !CONVERT_MIME.includes(mime)) { | |
// Preserve | |
} else if(PRESERVE_ALPHA && mime != "image/jpeg") { | |
updateButton(`Checking alpha...`); | |
canvas.width = image.width; | |
canvas.height = image.height; | |
ctx.drawImage(image, 0, 0); | |
let data = ctx.getImageData(0, 0, canvas.width, canvas.height).data, | |
hasAlpha = false; | |
for(let i = 3; i < data.length; i+=4) { | |
if(data[i] < 255) { | |
hasAlpha = true; | |
break; | |
} | |
} | |
if(hasAlpha) mime = "image/png"; | |
else mime = "image/jpeg"; | |
} else { | |
mime = "image/jpeg"; | |
} | |
const tryScale = async scale => { | |
const width = canvas.width = Math.floor(image.width * scale) || 1; | |
const height = canvas.height = Math.floor(image.height * scale) || 1; | |
if(xhr.aborted) throw new Error("Aborted"); | |
updateButton(`${width}x${height}`); | |
ctx.drawImage(image, 0, 0, width, height); | |
const blob = await new Promise(res => canvas.toBlob(res, mime, QUALITY)); | |
return blob; | |
} | |
const submit = blob => { | |
let filename = fileInput.files[0].name || "image.jpg"; | |
formData.set("upfile", blob, filename); | |
return; | |
} | |
// Skip full size png reencode | |
if(!(mime === "image/png" && max === 1)) { | |
let blob = await tryScale(max); | |
if(blob.size <= window.maxFilesize) return submit(blob); | |
} | |
for(let i = 0; i < MAX_TRIES; i++) { | |
const middle = (min + max) / 2; | |
let blob = await tryScale(middle); | |
if(blob.size <= window.maxFilesize && blob.size >= window.maxFilesize * (1 - SIZE_MARGIN)) { | |
return submit(blob); | |
} else if(blob.size > window.maxFilesize) { | |
max = middle; | |
} else { | |
min = middle; | |
} | |
} | |
submit(await tryScale(min)); | |
} | |
// Load attached image | |
function onFileChange() { | |
if(!fileInput.files[0]) return updateButton("Post"); | |
updateButton("Loading..."); | |
mime = fileInput.files[0].type; | |
image.onload = () => { | |
console.log(mime) | |
if(image.width > MAX_DIM || image.height > MAX_DIM || fileInput.files[0].size > window.maxFilesize) { | |
updateButton("Resize & Post", resizeSubmit); | |
} else if(CONVERT_MIME.includes(mime)) { | |
updateButton("Convert & Post", resizeSubmit); | |
} else { | |
updateButton("Post"); | |
} | |
}; | |
image.onerror = () => updateButton("Post"); | |
if(image.src) URL.revokeObjectURL(image.src); | |
image.src = URL.createObjectURL(fileInput.files[0]); | |
} | |
fileInput.addEventListener("change", onFileChange); | |
if(fileInput.files[0]) onFileChange(); | |
// File Input gets replaced when reset in quick reply for some reason | |
const removeObserver = new MutationObserver(mutations => { | |
for(const mutation of mutations) { | |
if(mutation.type !== "childList") continue; | |
for(const addedNode of mutation.addedNodes) { | |
if(addedNode.tagName === "INPUT" && addedNode.type === "file") { | |
fileInput = addedNode; | |
fileInput.addEventListener("change", onFileChange); | |
}; | |
} | |
} | |
}); | |
removeObserver.observe(fileInput.parentNode, { childList: true }) | |
} | |
// Watch for quick reply open | |
const observer = new MutationObserver(mutations => { | |
for(const mutation of mutations) { | |
if(mutation.type !== "childList") continue; | |
for(const node of mutation.addedNodes) { | |
if(node.id === "quickReply") registerForm(node.querySelector("form")); | |
} | |
} | |
}); | |
observer.observe(document.body, { childList: true }); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment