|
// ==UserScript== |
|
// @name 4chan /hdg/ catbox.moe userscript |
|
// @namespace 4chanhdgcatbox |
|
// @match https://boards.4chan.org/*/thread/* |
|
// @match https://archiveofsins.com/* |
|
// @match https://desuarchive.org/* |
|
// @grant GM.xmlHttpRequest |
|
// @grant GM_xmlhttpRequest |
|
// @version 4.4.2 |
|
// @author Anonymous |
|
// @description Upload image directly to catbox.moe from 4chan |
|
// @updateURL https://gist.github.com/raw/ca46eb79ce55e3216aecab49d5c7a3fb/catbox.user.js |
|
// @downloadURL https://gist.github.com/raw/ca46eb79ce55e3216aecab49d5c7a3fb/catbox.user.js |
|
// @require https://cdn.jsdelivr.net/npm/exif-js |
|
// @require https://cdn.jsdelivr.net/npm/exifreader@4.25.0/dist/exif-reader.min.js |
|
// @require https://cdn.jsdelivr.net/npm/pako@2.1.0/dist/pako.min.js |
|
// ==/UserScript== |
|
|
|
(async function () { |
|
function getLocalStorageSetting(key, defaultValue) { |
|
try { |
|
return JSON.parse(localStorage[key] || JSON.stringify(defaultValue)); |
|
} catch (e) { |
|
console.error(`Error parsing localStorage key "${key}":`, e); |
|
return defaultValue; |
|
} |
|
} |
|
|
|
let STRICT_CHECK = getLocalStorageSetting('CATBOX_HDG_STRICT_CHECK', false); |
|
let DONT_ATTACH_LINKS = getLocalStorageSetting('CATBOX_HDG_DONT_ATTACH_LINKS', false); |
|
let IMAGE_EXPANSION = getLocalStorageSetting('CATBOX_HDG_IMAGE_EXPANSION', true); |
|
let IMAGE_HOVER = getLocalStorageSetting('CATBOX_HDG_IMAGE_HOVER', true); |
|
let ATTACH_LINKS_WITH_EMBED = getLocalStorageSetting('CATBOX_HDG_ATTACH_LINKS_WITH_EMBED', false); |
|
let ONLY_REPLACE_SMALL_ATTACH = getLocalStorageSetting('CATBOX_HDG_ONLY_REPLACE_SMALL_ATTACH', true); |
|
let ATTACH_LINKS_WITH_BRACKETS = getLocalStorageSetting('CATBOX_HDG_ATTACH_LINKS_WITH_BRACKETS', false); |
|
|
|
const TITLE_CHECK = [ |
|
'/aids/', '/asdg/', '/ddg/', '/hdg/', '/sdg/', '/swarm/', '/vtai/', |
|
'/aicg/', |
|
'>nai leak speedrun', 'otaku ai art thread', |
|
['/jp/', ' ai thread '], ['/jp/', ' ai art '], ['/pw/', 'waifu wrestling alliance'], ['/bant/', 'ai waifus general'] |
|
]; |
|
const OP_CHECK = [ |
|
'waifus.nemusona.com', 'rentry.org/voldy', 'rentry.co/voldy', 'github.com/AUTOMATIC1111/stable-diffusion-webui' |
|
]; |
|
const ARCHIVE_CHECK = [ |
|
'archiveofsins.com', 'desuarchive.org' |
|
]; |
|
const CATBOX_BUTTON_ID = 'qr-catbox-button_userscript'; |
|
const RE_CATBOX_URL = /^https?:\/\/(?:files|litter)\.catbox\.moe\/([a-z0-9]{6}\.(?:png|jpe?g|webp|avif))$/i; |
|
const RE_CATBOX_FILENAME = /^catbox_[a-z0-9]{6}\.(?:png|jpe?g|webp)$/i; |
|
const RE_NAI_FILENAME = /^.+\ss\-\d+\s*(?:\(\d+\)|\-\d{1,2})?(\.png)+\s*$/; |
|
const RE_RESOLUTION = /([0-9]+)x([0-9]+)\)$/; |
|
|
|
const RE_CATBOX_ONLY_POST = ((dontIncludeBracketCheck) => { |
|
const basePattern = RE_CATBOX_URL.source.slice(1, -1); // Remove the ^ and $ anchors |
|
return new RegExp( |
|
!dontIncludeBracketCheck ? `(?<!<)(${basePattern})(?!>)` : `(${basePattern})`, |
|
'i' |
|
); |
|
})(ATTACH_LINKS_WITH_BRACKETS); |
|
|
|
let IS_4CHAN_XT = false; |
|
|
|
let loaded = false; |
|
let thread_match = false; |
|
let qr_updated = false; |
|
|
|
const posts = new Set(); |
|
|
|
let previewDiv = null; |
|
|
|
function log(msg) { |
|
console.log(`[4chan /sdg/ catbox.moe userscript] ${msg}`); |
|
} |
|
|
|
let dropEventPatched = false; |
|
try { |
|
log('Trying to override drop event listener'); |
|
const originalAddEventListener = EventTarget.prototype.addEventListener; |
|
EventTarget.prototype.addEventListener = function (type, listener, options) { |
|
if (type === 'drop' && this === document && listener?.name === 'dropFile') { |
|
log('Drop event listener found -- modifying'); |
|
const modifiedListener = function (event) { |
|
if (event.ctrlKey && event.shiftKey) { |
|
log('4chanX drop event listener intercepted by /hdg/ catbox.moe userscript'); |
|
return; |
|
} |
|
|
|
listener.call(this, event); |
|
}; |
|
originalAddEventListener.call(this, type, modifiedListener, options); |
|
return; |
|
} |
|
return originalAddEventListener.call(this, type, listener, options); |
|
}; |
|
log('Successfully patched drop event listener'); |
|
dropEventPatched = true; |
|
} catch { |
|
log('Failed to patch drop event listener'); |
|
} |
|
|
|
function getXmlHttpRequest() { |
|
return (typeof GM !== "undefined" && GM !== null ? GM.xmlHttpRequest : GM_xmlhttpRequest); |
|
} |
|
|
|
function get(url) { |
|
return new Promise((resolve, reject) => { |
|
getXmlHttpRequest()({ |
|
method: 'GET', |
|
url: url, |
|
timeout: 15000, |
|
responseType: 'blob', |
|
onload: function(response) { |
|
resolve(response); |
|
}, |
|
onerror: function(error) { |
|
reject(error); |
|
}, |
|
ontimeout: function(error) { |
|
reject(error); |
|
} |
|
}); |
|
}); |
|
} |
|
|
|
function post(url, data) { |
|
return new Promise((resolve, reject) => { |
|
getXmlHttpRequest()({ |
|
method: 'POST', |
|
url: url, |
|
data: data, |
|
timeout: 15000, |
|
onload: function(response) { |
|
resolve(response); |
|
}, |
|
onerror: function(error) { |
|
reject(error); |
|
}, |
|
ontimeout: function(error) { |
|
reject(error); |
|
} |
|
}); |
|
}); |
|
} |
|
|
|
async function toDataURL(url) { |
|
const blob = await fetch(url).then(res => res.blob()); |
|
return URL.createObjectURL(blob); |
|
} |
|
|
|
async function download(url, filename) { |
|
const a = document.createElement("a"); |
|
a.href = await toDataURL(url); |
|
a.download = filename; |
|
document.body.appendChild(a); |
|
a.click(); |
|
document.body.removeChild(a); |
|
} |
|
|
|
function transformCatboxLink(url) { |
|
if (url.includes("//litter.")) return url; |
|
if (url.includes(".avif")) return url; |
|
if (url.includes("/thumbs/t_")) { |
|
return url.replace("/thumbs/t_", "/"); |
|
} else { |
|
const lastSlashIndex = url.lastIndexOf("/"); |
|
return url.slice(0, lastSlashIndex) + "/thumbs/t_" + url.slice(lastSlashIndex + 1); |
|
} |
|
} |
|
|
|
function setupImageHover(imageElement) { |
|
// Adapted from https://github.com/ccd0/4chan-x/blob/920bd1ebc9f57e521eca95d7c90a9875bb2f17b1/src/General/UI.coffee#L348 |
|
// Thanks, GPT 4o |
|
if (!imageElement) { |
|
return; |
|
} |
|
|
|
const hoverPadding = 25; // Padding for preview placement |
|
|
|
function showPreview(event) { |
|
if (previewDiv) return; // Prevent multiple previews |
|
|
|
const { target } = event; |
|
const clientWidth = window.innerWidth; |
|
const clientHeight = window.innerHeight; |
|
|
|
// Create the preview container |
|
previewDiv = document.createElement('div'); |
|
previewDiv.style.position = 'fixed'; |
|
previewDiv.style.zIndex = 1000; |
|
previewDiv.style.pointerEvents = 'none'; |
|
if (target.classList.contains('full-image')) previewDiv.style.display = 'none'; |
|
|
|
if (!IMAGE_HOVER) return; |
|
|
|
// Create the preview image |
|
const previewImage = document.createElement('img'); |
|
previewImage.src = transformCatboxLink(target.src); |
|
previewImage.style.maxWidth = `${clientWidth - hoverPadding * 2}px`; |
|
previewImage.style.maxHeight = `${clientHeight - hoverPadding * 2}px`; |
|
previewImage.style.display = 'block'; |
|
|
|
previewDiv.appendChild(previewImage); |
|
document.body.appendChild(previewDiv); |
|
|
|
positionPreview(event); |
|
} |
|
|
|
function positionPreview(event) { |
|
if (!previewDiv) return; |
|
if (!IMAGE_HOVER) return; |
|
|
|
const { clientX, clientY } = event; |
|
const clientWidth = window.innerWidth; |
|
const clientHeight = window.innerHeight; |
|
|
|
const height = previewDiv.offsetHeight + hoverPadding; |
|
const width = previewDiv.offsetWidth; |
|
|
|
// Calculate vertical position |
|
const top = Math.max(0, Math.min(clientHeight - height, clientY - 120)); |
|
|
|
// Calculate horizontal position using the threshold logic |
|
const threshold = Math.max(clientWidth / 2, clientWidth - 400); |
|
const marginX = clientX <= threshold |
|
? clientX + 45 |
|
: clientWidth - clientX + 45; |
|
|
|
const adjustedMarginX = Math.min(marginX, clientWidth - width); |
|
|
|
const left = clientX <= threshold ? `${adjustedMarginX}px` : ''; |
|
const right = clientX > threshold ? `${adjustedMarginX}px` : ''; |
|
|
|
// Apply position |
|
previewDiv.style.top = `${top}px`; |
|
previewDiv.style.left = left; |
|
previewDiv.style.right = right; |
|
} |
|
|
|
function hidePreview() { |
|
if (!IMAGE_HOVER) return; |
|
if (previewDiv) { |
|
document.body.removeChild(previewDiv); |
|
previewDiv = null; |
|
} |
|
} |
|
|
|
// Attach event listeners to the provided image element |
|
imageElement.addEventListener('mouseenter', showPreview); |
|
imageElement.addEventListener('mousemove', positionPreview); |
|
imageElement.addEventListener('click', positionPreview); |
|
imageElement.addEventListener('mouseleave', hidePreview); |
|
imageElement.onclick = function (evt) { |
|
if (!IMAGE_EXPANSION) return; |
|
evt.preventDefault(); |
|
if (this.classList.contains('full-image')) { |
|
this.src = transformCatboxLink(this.src); |
|
previewDiv.style.display = 'block'; |
|
this.classList.remove('full-image'); |
|
this.style.maxWidth = '125px'; |
|
this.style.maxHeight = '125px'; |
|
} else { |
|
this.src = transformCatboxLink(this.src); |
|
previewDiv.style.display = 'none'; |
|
this.classList.add('full-image'); |
|
this.style.maxWidth = ''; |
|
this.style.maxHeight = ''; |
|
} |
|
}; |
|
} |
|
|
|
function insertTextWithSpacing(textarea, textToInsert, addBrackets=false) { |
|
const start = textarea.selectionStart; |
|
const end = textarea.selectionEnd; |
|
|
|
const value = textarea.value; |
|
|
|
const before = value[start - 1]; |
|
const after = value[end]; |
|
const needsSpaceBefore = before && !/\s/.test(before); |
|
const needsSpaceAfter = after && !/\s/.test(after); |
|
|
|
let prefix = needsSpaceBefore ? " " : ""; |
|
let suffix = needsSpaceAfter ? " " : ""; |
|
if (addBrackets) { |
|
prefix += "<"; |
|
suffix = ">" + suffix; |
|
} |
|
const newText = prefix + textToInsert + suffix; |
|
textarea.value = value.slice(0, start) + newText + value.slice(end); |
|
|
|
// Set the cursor position after the inserted text |
|
const newCursorPosition = start + newText.length; |
|
textarea.setSelectionRange(newCursorPosition, newCursorPosition); |
|
|
|
// Notify any potential event listeners |
|
textarea.dispatchEvent(new Event("input")); |
|
} |
|
|
|
function setFontColor(elm, color) { |
|
elm.setAttribute('style', elm.getAttribute('style').replace(/(.+color: )[a-z]+( !.+)/i, `$1${color}$2`)); |
|
} |
|
|
|
function setCatboxAuth() { |
|
const userhash = window.prompt('Catbox auth hotkey triggered. Please enter your userhash to store your login. Input nothing to remove it.'); |
|
localStorage['CATBOX_USERHASH'] = JSON.stringify(userhash || ''); |
|
} |
|
|
|
function getCatboxAuth() { |
|
const res = JSON.parse(localStorage['CATBOX_USERHASH'] || '{}'); |
|
return (typeof res === 'object' ? '' : res || ''); |
|
} |
|
|
|
async function uploadToCatbox(file, textOnly=false, textOnlyBrackets=false) { |
|
const catboxButton = document.querySelector(`#${CATBOX_BUTTON_ID}`); |
|
const submitButton = document.querySelector('#file-n-submit [type=submit][value=Submit]'); |
|
|
|
const formData = new FormData(); |
|
formData.append('reqtype', 'fileupload'); |
|
formData.append('fileToUpload', file, file.name.replace(/\.jpeg$/, ".jpg")); |
|
const userhash = getCatboxAuth(); |
|
if (userhash) { |
|
formData.append('userhash', userhash); |
|
} |
|
|
|
catboxButton.value = 'Uploading ...'; |
|
setFontColor(catboxButton, 'yellow'); |
|
|
|
// Selector doesn't seem to grab the submit button all the time for some reason |
|
if (submitButton) { |
|
submitButton.disabled = true; |
|
submitButton.style.pointerEvents = 'none'; |
|
submitButton.style.opacity = 0.25; |
|
} |
|
|
|
try { |
|
log('attempting catbox upload'); |
|
const response = await post('https://catbox.moe/user/api.php', formData); |
|
|
|
if (response.status === 200 && response.responseText.match(RE_CATBOX_URL)) { |
|
const filenameMatch = response.responseText.match(RE_CATBOX_URL); |
|
|
|
log('uploaded'); |
|
catboxButton.value = 'uploaded'; |
|
setFontColor(catboxButton, 'limegreen'); |
|
|
|
if (textOnly) { |
|
insertTextWithSpacing(document.querySelector('#qr textarea'), filenameMatch[0], textOnlyBrackets); |
|
} else { |
|
const filenameInput = document.querySelector('#qr-filename'); |
|
const fileEvent = new CustomEvent('QRSetFile', { |
|
detail: { |
|
file: file |
|
} |
|
}); |
|
|
|
document.dispatchEvent(fileEvent); |
|
|
|
filenameInput.value = `catbox_${filenameMatch[1].replace(/\.jpeg$/, ".jpg")}`; |
|
filenameInput.dispatchEvent(new Event('input', {bubbles:true})); |
|
} |
|
} else { |
|
log('upload error'); |
|
console.error(response); |
|
|
|
catboxButton.value = 'upload error'; |
|
setFontColor(catboxButton, 'red'); |
|
} |
|
} catch(err) { |
|
log('upload failed'); |
|
console.error(err); |
|
|
|
catboxButton.value = 'upload failed'; |
|
setFontColor(catboxButton, 'red'); |
|
} |
|
|
|
if (submitButton) { |
|
submitButton.disabled = false; |
|
submitButton.style.pointerEvents = ''; |
|
submitButton.style.opacity = 1; |
|
} |
|
|
|
setTimeout(() => { |
|
catboxButton.value = 'catbox'; |
|
setFontColor(catboxButton, 'inherit'); |
|
}, 2000); |
|
} |
|
|
|
const loadImage = (blob) => { |
|
return new Promise((resolve, reject) => { |
|
const image = new Image(); |
|
image.onload = () => resolve(image); |
|
image.onerror = reject; |
|
image.src = URL.createObjectURL(blob); |
|
}); |
|
} |
|
|
|
function parseNaiMetadata(metadata) { |
|
metadata = JSON.parse(metadata); |
|
return JSON.stringify(metadata, null, '\t'); |
|
} |
|
|
|
async function handleMetadataReq(evt, el, href) { |
|
evt.preventDefault(); |
|
|
|
const box = insertPromptBox(el); |
|
|
|
let metadata, chunks; |
|
const hostedOn4chan = el.href.includes('4cdn.org'); |
|
|
|
async function getStealth(href) { |
|
const res = await get(href); |
|
if (res.status != 200) { |
|
return; |
|
} |
|
|
|
const image = await loadImage(res.response); |
|
|
|
metadata = readInfoFromImageStealth(image); |
|
if (metadata) { |
|
try { |
|
let stealthMetadata = JSON.parse(metadata); |
|
if (stealthMetadata?.Comment) { |
|
return parseNaiMetadata(stealthMetadata['Comment']); |
|
} |
|
else if (stealthMetadata?.parameters) { |
|
return stealthMetadata.parameters; |
|
} |
|
else if (stealthMetadata?.prompt || stealthMetadata?.workflow) { |
|
return `prompt\n \n${stealthMetadata?.prompt}\n \nworkflow\n \n${stealthMetadata?.workflow}`; |
|
} |
|
} catch { |
|
return metadata; |
|
} |
|
} |
|
} |
|
|
|
if (hostedOn4chan) { |
|
metadata = await getStealth(href); |
|
} else { |
|
const res = await fetch(href); |
|
if (!res.ok || !res.body) { |
|
return; |
|
} |
|
|
|
const reader = res.body.getReader(); |
|
|
|
chunks = []; |
|
let iterCount = 0; |
|
|
|
while (true) { |
|
const {done, value} = await reader.read(); |
|
if (done || iterCount > 10) { |
|
break; |
|
} |
|
chunks.push(value); |
|
iterCount++; |
|
} |
|
|
|
if (href.endsWith('.jpg') || href.endsWith('.jpeg')) { |
|
let rawJpegMetdata = chunksToArray(chunks); |
|
rawJpegMetdata = EXIF.readFromBinaryFile(rawJpegMetdata.buffer.slice(rawJpegMetdata.byteOffset, rawJpegMetdata.byteLength + rawJpegMetdata.byteOffset)); |
|
if (Object.keys(rawJpegMetdata).includes('UserComment')) { |
|
const jpegMetadata = String.fromCharCode(...rawJpegMetdata.UserComment.slice(9).filter((value) => value !== 0)); |
|
updatePromptBox(box, jpegMetadata); |
|
} |
|
|
|
return; |
|
} |
|
|
|
if (href.endsWith('.webp') || href.endsWith('.avif')) { |
|
let exifReaderMeta = chunksToArray(chunks)?.buffer; |
|
exifReaderMeta = ExifReader.load(exifReaderMeta); |
|
if (Object.keys(exifReaderMeta).includes('UserComment')) { |
|
const exifMeta = String.fromCharCode(...exifReaderMeta.UserComment.value.slice(9).filter((value) => value !== 0)); |
|
updatePromptBox(box, exifMeta); |
|
} |
|
|
|
return; |
|
} |
|
|
|
metadata = await getMetaData(chunks[0]); |
|
} |
|
|
|
if (metadata) { |
|
updatePromptBox(box, metadata, hostedOn4chan || metadata.includes('"prompt": "')); |
|
return; |
|
} else if (!hostedOn4chan) { |
|
const fallbackMetadata = await getMetaData(chunksToArray(chunks)) || await getStealth(href); |
|
if (fallbackMetadata) { |
|
updatePromptBox(box, fallbackMetadata); |
|
return; |
|
} |
|
} |
|
|
|
updatePromptBox(box, 'No metadata found.'); |
|
} |
|
|
|
function updateLinkHover(link) { |
|
link.setAttribute('style', 'color: limegreen !important;'); |
|
link.addEventListener('mouseenter', (evt) => {evt.target.style.filter = 'brightness(250%)'}); |
|
link.addEventListener('mouseleave', (evt) => {evt.target.style.filter = 'none'}); |
|
} |
|
|
|
function updateFilenameLink(link) { |
|
const name = link.getAttribute('download')?.split('_')[1] || link.getAttribute('title')?.split('_')[1]; |
|
if (!name) { |
|
log('Failed to parse catbox link filename'); |
|
return; |
|
} |
|
|
|
// Stupid way to do this but I'm too lazy to refactor. |
|
let href = `https://files.catbox.moe/${name}`; |
|
if (link.href.includes('://litter.')) { |
|
href = `https://litter.catbox.moe/${name}`; |
|
} |
|
|
|
log(`parsed catbox link: ${href}`); |
|
link.href = href; |
|
updateLinkHover(link); |
|
} |
|
|
|
async function updateDownloadLinks(root=null, limit=100000) { |
|
const partial = root !== null; |
|
root = root !== null ? root : document; |
|
const skipExisting = (root == document); |
|
|
|
function getPostId(el) { |
|
return el.closest('[data-full-i-d]').getAttribute('data-full-i-d').replace(/\D/g, ''); |
|
} |
|
|
|
if (!DONT_ATTACH_LINKS && partial) { |
|
setupImageHover(root.querySelector('img.catbox-image-embed')); |
|
const a = root.querySelector('.download-button-catbox'); |
|
if (a) { |
|
a.onclick = function(evt) { |
|
evt.preventDefault(); |
|
download(evt.target.href, evt.target.download); |
|
} |
|
} |
|
} |
|
|
|
function shouldReplaceAttachment(fileEl) { |
|
if (!ATTACH_LINKS_WITH_EMBED) { |
|
return false; |
|
} |
|
|
|
if (!ONLY_REPLACE_SMALL_ATTACH) { |
|
return true; |
|
} |
|
|
|
if (!fileEl) { |
|
return false; |
|
} |
|
|
|
const m = fileEl.querySelector('.file-info').innerText.trim().match(RE_RESOLUTION); |
|
if (!m) { |
|
return false; |
|
} |
|
|
|
const width = parseInt(m[1]); |
|
const height = parseInt(m[2]); |
|
|
|
if (ONLY_REPLACE_SMALL_ATTACH && width <= 512 && height <= 512) { |
|
return true; |
|
} |
|
|
|
return false; |
|
} |
|
|
|
if (!DONT_ATTACH_LINKS && !partial) { |
|
const catboxLinkOnlyPosts = [ |
|
...Array.from(root.querySelectorAll('.post:not(.op) .postInfo + blockquote')) |
|
.map(el => { |
|
const match = RE_CATBOX_ONLY_POST.exec(el.textContent); |
|
return match ? [el, match[1], match[2], 'LINK_ONLY'] : null; |
|
}), |
|
...Array.from(root.querySelectorAll('.post:not(.op) .postInfo + .file + blockquote')) |
|
.map(el => { |
|
const match = RE_CATBOX_ONLY_POST.exec(el.textContent); |
|
return match && shouldReplaceAttachment(el.previousElementSibling) ? [el, match[1], match[2], 'ATTACHMENT'] : null; |
|
}) |
|
] |
|
.filter(item => item !== null); |
|
|
|
catboxLinkOnlyPosts.forEach(([post, url, filename, linkType]) => { |
|
const postId = getPostId(post); |
|
|
|
// Remove bare links from post |
|
const links = post.querySelectorAll('a'); |
|
links.forEach(link => { |
|
if (link.getAttribute('href') === url) { |
|
const span = document.createElement('span'); |
|
span.textContent = '[picrel]'; |
|
span.style.opacity = '0.5'; |
|
span.style.cursor = 'help'; |
|
span.title = '(Automatically attached Catbox link to this post via the /hdg/ catbox.moe userscript.)'; |
|
link.replaceWith(span); |
|
} |
|
if (link.getAttribute('data-href') === url) { |
|
link.remove(); |
|
} |
|
}); |
|
|
|
if (linkType === 'LINK_ONLY' || (ATTACH_LINKS_WITH_EMBED && linkType === 'ATTACHMENT')) { |
|
// Create the container <div> with class "file" and unique ID |
|
const fileDiv = document.createElement('div'); |
|
fileDiv.className = 'file'; |
|
fileDiv.id = `f${postId}`; |
|
|
|
// Create the fileText <div> |
|
const fileTextDiv = document.createElement('div'); |
|
fileTextDiv.className = 'fileText'; |
|
fileTextDiv.id = `fT${postId}`; |
|
|
|
// Create the file-info <span> |
|
const fileInfoSpan = document.createElement('span'); |
|
fileInfoSpan.className = 'file-info'; |
|
fileInfoSpan.classList.add('file-info-catbox'); |
|
|
|
// Create the first <a> element for the link |
|
const fileLink = document.createElement('a'); |
|
fileLink.href = url; |
|
fileLink.target = '_blank'; |
|
fileLink.textContent = `catbox_${filename}`; |
|
fileLink.classList.add('file-link-catbox'); |
|
|
|
// Create the second <a> element for the download button |
|
const downloadLink = document.createElement('a'); |
|
downloadLink.href = url; |
|
downloadLink.download = `catbox_${filename}`; |
|
if (IS_4CHAN_XT) { |
|
downloadLink.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 512 512"><path d="M288 32c0-17.7-14.3-32-32-32s-32 14.3-32 32V274.7l-73.4-73.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l128 128c12.5 12.5 32.8 12.5 45.3 0l128-128c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L288 274.7V32zM64 352c-35.3 0-64 28.7-64 64v32c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V416c0-35.3-28.7-64-64-64H346.5l-45.3 45.3c-25 25-65.5 25-90.5 0L165.5 352H64zm368 56a24 24 0 1 1 0 48 24 24 0 1 1 0-48z" fill="currentColor"></path></svg>`; |
|
} else { |
|
downloadLink.className = 'fa fa-download download-button'; |
|
} |
|
downloadLink.classList.add('download-button-catbox'); |
|
downloadLink.onclick = function(evt) { |
|
evt.preventDefault(); |
|
download(url, `catbox_${filename}`); |
|
} |
|
|
|
// Append the links to the file-info <span> |
|
fileInfoSpan.appendChild(document.createTextNode("*")); |
|
fileInfoSpan.appendChild(fileLink); |
|
fileInfoSpan.appendChild(document.createTextNode(" ")); |
|
fileInfoSpan.appendChild(downloadLink); |
|
|
|
// Append the file-info <span> to the fileText <div> |
|
fileTextDiv.appendChild(fileInfoSpan); |
|
|
|
// Create the thumbnail <a> element |
|
const fileThumb = document.createElement('a'); |
|
fileThumb.className = 'fileThumb'; |
|
fileThumb.href = url; |
|
fileThumb.target = '_blank'; |
|
|
|
// Create the <img> element for the thumbnail |
|
const fileThumbImg = document.createElement('img'); |
|
fileThumbImg.src = transformCatboxLink(url); |
|
fileThumbImg.style.maxWidth = '125px'; |
|
fileThumbImg.style.maxHeight = '125px'; |
|
fileThumbImg.loading = 'lazy'; |
|
fileThumbImg.className = 'catbox-image-embed'; |
|
|
|
setupImageHover(fileThumbImg); |
|
|
|
// Append the <img> to the thumbnail <a> |
|
fileThumb.appendChild(fileThumbImg); |
|
|
|
// Append the fileText and fileThumb to the main container <div> |
|
fileDiv.appendChild(fileTextDiv); |
|
fileDiv.appendChild(fileThumb); |
|
|
|
// Insert the new structure before the blockquote |
|
post.parentNode.insertBefore(fileDiv, post); |
|
} |
|
}); |
|
} |
|
|
|
const downloadLinks = Array.from(root.querySelectorAll('.file-info :is(a[href*="4cdn.org"][download^="catbox_" i], a[href*=".catbox.moe"][download])') || []).slice(0, limit); |
|
const naiLinks = Array.from(root.querySelectorAll('a:is([download*="s-1"],[download*="s-2"],[download*="s-3"],[download*="s-4"],[download*="s-5"],[download*="s-6"],[download*="s-7"],[download*="s-8"],[download*="s-9"])[download$="png"]') || []).slice(0, limit); |
|
const fileDownloadLinks = Array.from(root.querySelectorAll('.file-info a.download-button[href]:not([href*=".catbox.moe/"]):not([download^="catbox_" i])') || []).slice(0, limit); |
|
|
|
for (const link of [...downloadLinks, ...naiLinks]) { |
|
if (skipExisting && posts.has(getPostId(link))) { |
|
continue; |
|
} |
|
|
|
if (link.getAttribute('download').match(RE_CATBOX_FILENAME)) { |
|
updateFilenameLink(link); |
|
continue; |
|
} |
|
|
|
if (link.getAttribute('download').match(RE_NAI_FILENAME)) { |
|
updateLinkHover(link); |
|
} |
|
} |
|
|
|
const catboxLinks = Array.from(root.querySelectorAll(':is(blockquote, .file-info) a[href*=".catbox.moe/"]:is([href$=".png" i], [href$=".jpg" i], [href$=".jpeg" i], [href$=".webp" i], [href$=".avif" i]):not(.file-link-catbox)')).slice(0, limit); |
|
|
|
for (const link of [...catboxLinks, ...fileDownloadLinks]) { |
|
if (skipExisting && posts.has(getPostId(link))) { |
|
continue; |
|
} |
|
|
|
log(`found existing link: ${link.href}`); |
|
link.style.cursor = 'help'; |
|
link.title = 'Right click to attempt to load & show/hide metadata'; |
|
link.addEventListener('contextmenu', async (evt) => {await handleMetadataReq(evt, link, link.href)}); |
|
} |
|
|
|
if (skipExisting) { |
|
for (const link of [...catboxLinks, ...fileDownloadLinks]) { |
|
posts.add(getPostId(link)); |
|
} |
|
} |
|
} |
|
|
|
function createMetadataBoxButton(name, desc, rgb, idx=0) { |
|
const btn = document.createElement('div'); |
|
btn.title = desc; |
|
btn.classList.add(`catbox-prompt-${name}_userscript`); |
|
btn.style.position = 'absolute'; |
|
btn.style.width = '16px'; |
|
btn.style.height = '16px'; |
|
btn.style.backgroundColor = `rgba(${rgb[0]},${rgb[1]},${rgb[2]},0.5)`; |
|
btn.style.lineHeight = '16px'; |
|
btn.style.textAlign = 'center'; |
|
btn.style.marginTop = '-8px'; |
|
btn.style.marginLeft = '-8px'; |
|
btn.style.textIndent = '0px'; |
|
btn.style.userSelect = 'none'; |
|
btn.style.cursor = 'pointer'; |
|
btn.style.color = 'white'; |
|
btn.style.left = `${((idx + 1) * 16) + (2 * (idx > 0 ? 1 : 0))}px`; |
|
btn.style.top = '16px'; |
|
btn.innerText = name.charAt(0).toUpperCase(); |
|
btn.addEventListener('mouseenter', (evt) => {evt.target.style.backgroundColor = `rgba(${rgb[0]},${rgb[1]},${rgb[2]},0.8)`}); |
|
btn.addEventListener('mouseleave', (evt) => {evt.target.style.backgroundColor = `rgba(${rgb[0]},${rgb[1]},${rgb[2]},0.5)`}); |
|
|
|
return btn; |
|
} |
|
|
|
function insertPromptBox(el) { |
|
const boxClass = 'catbox-prompt_userscript'; |
|
// const blockquote = document.querySelector(`#m${el.closest('.postContainer[data-full-i-d]').getAttribute('data-full-i-d').split('h.')[1]}`); |
|
const blockquote = el.closest(':is(.reply, .op)').querySelector('blockquote'); |
|
|
|
const exists = blockquote.querySelector(`.${boxClass}`); |
|
if (exists) { |
|
exists.remove(); |
|
return; |
|
} |
|
|
|
const box = document.createElement('div'); |
|
box.classList.add('catbox-prompt_userscript'); |
|
box.style.display = 'grid'; |
|
box.style.position = 'relative'; |
|
box.style.color = window.getComputedStyle(document.body).color || 'white'; |
|
box.style.backgroundColor = 'rgba(0,0,255,0.1)'; |
|
box.style.border = '2px solid rgba(255,255,255,0.2)'; |
|
box.style.borderStyle = 'dashed'; |
|
box.style.padding = '8px'; |
|
box.style.paddingTop = '16px;' |
|
box.style.marginBottom = '16px'; |
|
box.style.whiteSpace = 'pre-wrap'; |
|
box.style.textIndent = '48px'; |
|
box.style.maxHeight = '480px'; |
|
box.style.overflowY = 'auto'; |
|
|
|
box.innerText = 'Attempting to load metadata...'; |
|
|
|
blockquote.prepend(box); |
|
|
|
return box; |
|
} |
|
|
|
function updatePromptBox(box, metadata, jsonDisplay = false) { |
|
if (jsonDisplay) { |
|
box.style.fontFamily = 'monospace'; |
|
} |
|
|
|
box.innerText = metadata; |
|
|
|
const closeBtn = createMetadataBoxButton('x', 'Hide', [255,0,0], 0); |
|
closeBtn.addEventListener('click', () => {box.remove()}); |
|
|
|
const copyBtn = createMetadataBoxButton('copy', 'Copy', [0,127,255], 1); |
|
copyBtn.addEventListener('click', async (evt) => { |
|
await navigator.clipboard.writeText(metadata); |
|
copyBtn.style.backgroundColor = 'rgba(0,255,0,0.5)'; |
|
}); |
|
|
|
box.appendChild(closeBtn); |
|
box.appendChild(copyBtn); |
|
} |
|
|
|
async function updateQrWindow() { |
|
if (qr_updated) { |
|
log('qr window already updated'); |
|
return; |
|
} |
|
|
|
const fileButton = document.querySelector('#qr-file-button'); |
|
|
|
const catboxButton = fileButton.cloneNode(); |
|
catboxButton.id = CATBOX_BUTTON_ID; |
|
catboxButton.value = 'Catbox'; |
|
catboxButton.setAttribute('style', 'text-transform: uppercase; font-size: 10px !important; border-radius: 2px !important; color: inherit !important;'); |
|
catboxButton.title = 'Tips:\nYou can directly upload to Catbox by dragging a file into the quick reply window while holding the Control key.\nDrag while holding Control + Shift to add the link only to the text of your reply.\nDrag while holding Control + Shift + Alt to add the link with angled brackets (to prevent auto-embedding).'; |
|
|
|
fileButton.parentNode.insertBefore(catboxButton, fileButton.nextSibling); |
|
|
|
catboxButton.addEventListener('click', async (evt) => { |
|
const input = document.createElement('input'); |
|
|
|
input.setAttribute('type', 'file'); |
|
input.setAttribute('style', 'display: none !important;'); |
|
input.addEventListener('change', async () => { |
|
if (input.files.length > 0) { |
|
await uploadToCatbox(input.files[0]); |
|
} |
|
input.remove(); |
|
}); |
|
|
|
document.body.appendChild(input); |
|
input.click(); |
|
}); |
|
|
|
qr_updated = true; |
|
} |
|
|
|
function textCheck(inp, arr) { |
|
for (let title of arr) { |
|
if (Array.isArray(title)) { |
|
if (title.every(substr => inp.includes(substr.toLowerCase()))) { |
|
return true; |
|
} |
|
} else if (inp.indexOf(title.toLowerCase()) != -1) { |
|
return true; |
|
} |
|
} |
|
|
|
return false; |
|
} |
|
|
|
async function init() { |
|
if (loaded) { |
|
return; |
|
} |
|
|
|
loaded = true; |
|
log('initialized userscript'); |
|
if (document.querySelector('html.fourchan-xt')) { |
|
IS_4CHAN_XT = true; |
|
log('4chan X fork detected: 4chan XT'); |
|
} |
|
|
|
const pageTitle = document.title.toLowerCase(); |
|
const opText = document.querySelector('.post.op .postMessage')?.innerText.toLowerCase() || ''; |
|
thread_match = !STRICT_CHECK || textCheck(pageTitle, TITLE_CHECK) || textCheck(opText, OP_CHECK); |
|
if (!thread_match) { |
|
log('Thread does not match criteria, short-circuiting'); |
|
return; |
|
} |
|
|
|
document.addEventListener('keydown', (evt) => { |
|
if (evt.key == "x" && evt.ctrlKey && evt.altKey) { |
|
setCatboxAuth(); |
|
} |
|
}); |
|
|
|
if (document.querySelector('#qr') && !qr_updated) { |
|
log('qr window already exists -- updating qr window'); |
|
await updateQrWindow(); |
|
} |
|
|
|
window.addEventListener('drop', async (evt) => { |
|
log('drop event'); |
|
|
|
if (!evt.dataTransfer.files.length || !evt.ctrlKey || (evt.shiftKey && !evt.ctrlKey)) { |
|
return; |
|
} |
|
|
|
evt.preventDefault(); |
|
|
|
uploadToCatbox(evt.dataTransfer.files[0], evt.shiftKey && dropEventPatched, evt.altKey); |
|
}); |
|
|
|
document.addEventListener('QRDialogCreation', async () => { |
|
log('QR dialog creation event -- attempting to update qr window'); |
|
|
|
await updateQrWindow(); |
|
}); |
|
|
|
await updateDownloadLinks(); |
|
document.addEventListener('PostsInserted', async (evt) => { |
|
if (evt.target.hasAttribute('data-full-i-d')) { |
|
await updateDownloadLinks(evt.target); |
|
} else { |
|
await updateDownloadLinks(); |
|
} |
|
}); |
|
} |
|
|
|
document.addEventListener('4chanXInitFinished', async () => { |
|
log('4chanX init finished'); |
|
await init(); |
|
}); |
|
|
|
// 4chanXInitFinished event doesn't seem to fire all the time |
|
window.addEventListener('load', async () => { |
|
if (ARCHIVE_CHECK.includes(window.location.hostname)) { |
|
return; |
|
} else { |
|
setTimeout(async () => { |
|
if (!loaded) { |
|
log('4chanX init never received, using fallback'); |
|
await init(); |
|
} |
|
}, 5000) |
|
} |
|
}); |
|
|
|
function fillArchiveWithLinks() { |
|
if (ARCHIVE_CHECK.includes(window.location.hostname)) { |
|
log('Archive website detected'); |
|
const catboxLinks = document.querySelectorAll('a.btnr[download^="catbox_" i]'); |
|
for (const link of catboxLinks) { |
|
updateFilenameLink(link); |
|
} |
|
} |
|
} |
|
|
|
if (document.readyState !== 'loading') { |
|
fillArchiveWithLinks(); |
|
} else { |
|
document.addEventListener('DOMContentLoaded', async () => { fillArchiveWithLinks(); }) |
|
} |
|
|
|
function chunksToArray(inp) { |
|
|
|
let data = inp; |
|
|
|
if (Array.isArray(inp)) { |
|
let length = 0; |
|
inp.forEach(item => { |
|
length += item.length; |
|
}); |
|
|
|
data = new Uint8Array(length); |
|
let offset = 0; |
|
inp.forEach(item => { |
|
data.set(item, offset); |
|
offset += item.length; |
|
}); |
|
|
|
return data; |
|
|
|
} else {return inp} |
|
} |
|
|
|
/* ----------------------------------------------------------------- |
|
/* https://github.com/moonshinegloss/stable-diffusion-discord-prompts |
|
/* ----------------------------------------------------------------- */ |
|
|
|
// Used for fast-ish conversion between uint8s and uint32s/int32s. |
|
// Also required in order to remain agnostic for both Node Buffers and |
|
// Uint8Arrays. |
|
let uint8 = new Uint8Array(4) |
|
let int32 = new Int32Array(uint8.buffer) |
|
let uint32 = new Uint32Array(uint8.buffer) |
|
|
|
const RESOLUTION_UNITS = {UNDEFINED: 0, METERS: 1, INCHES: 2}; |
|
|
|
/** |
|
* https://github.com/hughsk/png-chunk-text |
|
* Reads a Uint8Array or Node.js Buffer instance containing a tEXt PNG chunk's data and returns its keyword/text: |
|
* @param data |
|
* @returns {{text: string, keyword: string}} |
|
*/ |
|
function textDecode(data, name='tEXt') { |
|
if (data.data && data.name) { |
|
data = data.data; |
|
} |
|
|
|
let naming = true; |
|
let keywordBytes = []; |
|
let textBytes = []; |
|
|
|
for (let i = 0; i < data.length; i++) { |
|
const code = data[i]; |
|
|
|
if (naming) { |
|
if (code) { |
|
keywordBytes.push(code); |
|
} else { |
|
naming = false; |
|
} |
|
} else { |
|
if (code) { |
|
textBytes.push(code); |
|
} |
|
} |
|
} |
|
|
|
const decoder = new TextDecoder(name == 'tEXt' ? 'latin1' : 'utf8'); |
|
return { |
|
keyword: decoder.decode(new Uint8Array(keywordBytes)), |
|
text: decoder.decode(new Uint8Array(textBytes)), |
|
}; |
|
} |
|
|
|
|
|
/** |
|
* https://github.com/hughsk/png-chunks-extract |
|
* Extract the data chunks from a PNG file. |
|
* Useful for reading the metadata of a PNG image, or as the base of a more complete PNG parser. |
|
* Takes the raw image file data as a Uint8Array or Node.js Buffer, and returns an array of chunks. Each chunk has a name and data buffer: |
|
* @param data {Uint8Array} |
|
* @returns {[{name: String, data: Uint8Array}]} |
|
*/ |
|
function extractChunks (data) { |
|
|
|
if (data[0] !== 0x89) throw new Error('Invalid .png file header') |
|
if (data[1] !== 0x50) throw new Error('Invalid .png file header') |
|
if (data[2] !== 0x4E) throw new Error('Invalid .png file header') |
|
if (data[3] !== 0x47) throw new Error('Invalid .png file header') |
|
if (data[4] !== 0x0D) throw new Error('Invalid .png file header: possibly caused by DOS-Unix line ending conversion?') |
|
if (data[5] !== 0x0A) throw new Error('Invalid .png file header: possibly caused by DOS-Unix line ending conversion?') |
|
if (data[6] !== 0x1A) throw new Error('Invalid .png file header') |
|
if (data[7] !== 0x0A) throw new Error('Invalid .png file header: possibly caused by DOS-Unix line ending conversion?') |
|
|
|
let ended = false |
|
let chunks = [] |
|
let idx = 8 |
|
|
|
while (idx < data.length) { |
|
// Read the length of the current chunk, |
|
// which is stored as a Uint32. |
|
uint8[3] = data[idx++] |
|
uint8[2] = data[idx++] |
|
uint8[1] = data[idx++] |
|
uint8[0] = data[idx++] |
|
|
|
// Chunk includes name/type for CRC check (see below). |
|
let length = uint32[0] + 4 |
|
let chunk = new Uint8Array(length) |
|
chunk[0] = data[idx++] |
|
chunk[1] = data[idx++] |
|
chunk[2] = data[idx++] |
|
chunk[3] = data[idx++] |
|
|
|
// Get the name in ASCII for identification. |
|
let name = ( |
|
String.fromCharCode(chunk[0]) + |
|
String.fromCharCode(chunk[1]) + |
|
String.fromCharCode(chunk[2]) + |
|
String.fromCharCode(chunk[3]) |
|
) |
|
|
|
// The IHDR header MUST come first. |
|
if (!chunks.length && name !== 'IHDR') { |
|
throw new Error('IHDR header missing') |
|
} |
|
|
|
// The IEND header marks the end of the file, |
|
// so on discovering it break out of the loop. |
|
if (name === 'IEND') { |
|
ended = true |
|
chunks.push({ |
|
name: name, |
|
data: new Uint8Array(0) |
|
}) |
|
|
|
break |
|
} |
|
|
|
// Read the contents of the chunk out of the main buffer. |
|
for (let i = 4; i < length; i++) { |
|
chunk[i] = data[idx++] |
|
} |
|
|
|
// Read out the CRC value for comparison. |
|
// It's stored as an Int32. |
|
uint8[3] = data[idx++] |
|
uint8[2] = data[idx++] |
|
uint8[1] = data[idx++] |
|
uint8[0] = data[idx++] |
|
|
|
// The chunk data is now copied to remove the 4 preceding |
|
// bytes used for the chunk name/type. |
|
let chunkData = new Uint8Array(chunk.buffer.slice(4)) |
|
|
|
chunks.push({ |
|
name: name, |
|
data: chunkData |
|
}) |
|
} |
|
|
|
return chunks |
|
} |
|
|
|
/** |
|
* read 4 bytes number from UInt8Array. |
|
* @param uint8array |
|
* @param offset |
|
* @returns {number} |
|
*/ |
|
function readUint32 (uint8array,offset) { |
|
let byte1, byte2, byte3, byte4; |
|
byte1 = uint8array[offset++]; |
|
byte2 = uint8array[offset++]; |
|
byte3 = uint8array[offset++]; |
|
byte4 = uint8array[offset]; |
|
return 0 | (byte1 << 24) | (byte2 << 16) | (byte3 << 8) | byte4; |
|
} |
|
|
|
/** |
|
* Get object with PNG metadata. only tEXt and pHYs chunks are parsed |
|
* @param buffer {Buffer} |
|
* @returns {{tEXt: {keyword: value}, pHYs: {x: number, y: number, units: RESOLUTION_UNITS}, [string]: true}} |
|
*/ |
|
function readMetadata(buffer){ |
|
let result = {}; |
|
const chunks = extractChunks(buffer); |
|
chunks.forEach( chunk => { |
|
switch(chunk.name){ |
|
case 'tEXt': |
|
case 'iTXt': |
|
if (!result.tEXt) { |
|
result.tEXt = {}; |
|
} |
|
let textChunk = textDecode(chunk.data, chunk.name); |
|
result.tEXt[textChunk.keyword] = textChunk.text; |
|
break |
|
case 'pHYs': |
|
result.pHYs = { |
|
// Pixels per unit, X axis: 4 bytes (unsigned integer) |
|
"x": readUint32(chunk.data, 0), |
|
// Pixels per unit, Y axis: 4 bytes (unsigned integer) |
|
"y": readUint32(chunk.data, 4), |
|
"unit": chunk.data[8], |
|
} |
|
break |
|
case 'gAMA': |
|
case 'cHRM': |
|
case 'sRGB': |
|
case 'IHDR': |
|
case 'iCCP': |
|
default: |
|
result[chunk.name] = true; |
|
} |
|
}) |
|
return result; |
|
} |
|
|
|
function largeuint8ArrToString(uint8arr) { |
|
return new Promise((resolve) => { |
|
const f = new FileReader(); |
|
f.onload = function(e) { |
|
resolve(e.target.result); |
|
} |
|
f.readAsText(new Blob([uint8arr])); |
|
}) |
|
} |
|
|
|
function imageHasAlpha (context, canvas) { |
|
var data = context.getImageData(0, 0, canvas.width, canvas.height).data, |
|
hasAlphaPixels = false; |
|
for (var i = 3, n = data.length; i < n; i+=4) { |
|
if (data[i] < 255) { |
|
hasAlphaPixels = true; |
|
break; |
|
} |
|
} |
|
return hasAlphaPixels; |
|
} |
|
|
|
function readInfoFromImageStealth(image) { |
|
|
|
let geninfo, items, paramLen; |
|
let r, g, b, a; |
|
|
|
const canvas = document.createElement('canvas'); |
|
|
|
// trying to read stealth pnginfo |
|
const [width, height] = [image.width, image.height]; |
|
const context = canvas.getContext('2d'); |
|
canvas.width = image.width; |
|
canvas.height = image.height; |
|
context.drawImage(image, 0, 0); |
|
|
|
const imageData = context.getImageData(0, 0, width, height); |
|
const data = imageData.data; |
|
|
|
let hasAlpha = imageHasAlpha(context, canvas); |
|
let mode = null; |
|
let compressed = false; |
|
let binaryData = ''; |
|
let bufferA = ''; |
|
let bufferRGB = ''; |
|
let indexA = 0; |
|
let indexRGB = 0; |
|
let sigConfirmed = false; |
|
let confirmingSignature = true; |
|
let readingParamLen = false; |
|
let readingParam = false; |
|
let readEnd = false; |
|
|
|
for (let x = 0; x < width; x++) { |
|
for (let y = 0; y < height; y++) { |
|
|
|
let i = (y * width + x) * 4; |
|
|
|
if (hasAlpha) { |
|
[r, g, b, a] = data.slice(i, i+4); |
|
bufferA += (a & 1).toString(); |
|
indexA++; |
|
} else { |
|
[r, g, b] = data.slice(i, i+3); |
|
} |
|
bufferRGB += (r & 1).toString(); |
|
bufferRGB += (g & 1).toString(); |
|
bufferRGB += (b & 1).toString(); |
|
indexRGB += 3; |
|
|
|
if (confirmingSignature) { |
|
if (indexA === 'stealth_pnginfo'.length * 8) { |
|
const decodedSig = new TextDecoder().decode(new Uint8Array(bufferA.match(/\d{8}/g).map(b => parseInt(b, 2)))); |
|
if (decodedSig === 'stealth_pnginfo' || decodedSig === 'stealth_pngcomp') { |
|
confirmingSignature = false; |
|
sigConfirmed = true; |
|
readingParamLen = true; |
|
mode = 'alpha'; |
|
if (decodedSig === 'stealth_pngcomp') { |
|
compressed = true; |
|
} |
|
bufferA = ''; |
|
indexA = 0; |
|
} else { |
|
readEnd = true; |
|
break; |
|
} |
|
} else if (indexRGB === 'stealth_pnginfo'.length * 8) { |
|
const decodedSig = new TextDecoder().decode(new Uint8Array(bufferRGB.match(/\d{8}/g).map(b => parseInt(b, 2)))); |
|
if (decodedSig === 'stealth_rgbinfo' || decodedSig === 'stealth_rgbcomp') { |
|
confirmingSignature = false; |
|
sigConfirmed = true; |
|
readingParamLen = true; |
|
mode = 'rgb'; |
|
if (decodedSig === 'stealth_rgbcomp') { |
|
compressed = true; |
|
} |
|
bufferRGB = ''; |
|
indexRGB = 0; |
|
} |
|
} |
|
} else if (readingParamLen) { |
|
if (mode === 'alpha' && indexA === 32) { |
|
paramLen = parseInt(bufferA, 2); |
|
readingParamLen = false; |
|
readingParam = true; |
|
bufferA = ''; |
|
indexA = 0; |
|
} else if (mode != 'alpha' && indexRGB === 33) { |
|
paramLen = parseInt(bufferRGB.slice(0, -1), 2); |
|
readingParamLen = false; |
|
readingParam = true; |
|
bufferRGB = bufferRGB.slice(-1); |
|
indexRGB = 1; |
|
} |
|
} else if (readingParam) { |
|
if (mode === 'alpha' && indexA === paramLen) { |
|
binaryData = bufferA; |
|
readEnd = true; |
|
break; |
|
} |
|
else if (mode != 'alpha' && indexRGB >= paramLen) { |
|
const diff = paramLen - indexRGB; |
|
if (diff < 0) { |
|
bufferRGB = bufferRGB.slice(0, diff); |
|
} |
|
binaryData = bufferRGB; |
|
readEnd = true; |
|
break; |
|
} |
|
} else { |
|
// Impossible |
|
readEnd = true; |
|
break; |
|
} |
|
} |
|
|
|
if (readEnd) { |
|
break; |
|
} |
|
} |
|
|
|
if (sigConfirmed && binaryData) { |
|
// Convert binary string to UTF-8 encoded text |
|
const byteData = new Uint8Array(binaryData.match(/\d{8}/g).map(b => parseInt(b, 2))); |
|
let decodedData; |
|
if (compressed) { |
|
decodedData = pako.inflate(byteData, {to: 'string'}); |
|
} else { |
|
decodedData = new TextDecoder().decode(byteData); |
|
} |
|
geninfo = decodedData; |
|
} |
|
|
|
return geninfo; |
|
} |
|
|
|
async function getMetaData(chunks) { |
|
let meta |
|
try{ |
|
meta = readMetadata(chunks) |
|
}catch(_){} |
|
|
|
if (meta?.tEXt?.Comment && meta?.tEXt?.Description && meta?.tEXt?.Software) { |
|
return parseNaiMetadata(meta.tEXt.Comment); |
|
} |
|
if(meta?.tEXt?.Dream) { |
|
return `${meta?.tEXt?.Dream} ${meta?.tEXt?.['sd-metadata'] || ''}` |
|
}else if(meta?.tEXt?.parameters) { |
|
return meta?.tEXt?.parameters |
|
}else if(meta?.tEXt?.prompt || meta?.tEXt?.workflow) { |
|
return `prompt\n \n${meta?.tEXt?.prompt}\n \nworkflow\n \n${meta?.tEXt?.workflow}`; |
|
} else if(meta?.tEXt?.chara) { |
|
let charaDef = atob(meta?.tEXt?.chara); |
|
let charaDefJson = JSON.parse(charaDef); |
|
if (charaDefJson && ['name', 'description', 'mes_example', 'first_mes'].every(val => Object.keys(charaDefJson).includes(val))) { |
|
return `Name: ${charaDefJson['name']}\n \nDescription: ${charaDefJson['description']}\n \nMessage example: ${charaDefJson['mes_example']}\n \nFirst message: ${charaDefJson['first_mes']}`; |
|
} |
|
} |
|
|
|
// fallback to simple text extraction |
|
const textData = await largeuint8ArrToString(chunks) |
|
const textTypes = ["Dream","parameters"] |
|
|
|
if(textData.includes("IDAT") && textTypes.some(x => textData.includes(x))) { |
|
const result = textData.split("IDAT")[0] |
|
.replace(new RegExp(`[\\s\\S]+Xt(${textTypes.join('|')})`),"") |
|
.replace(/[^\x00-\x7F]/g,"") |
|
|
|
if(result.length > 50) return result |
|
} |
|
|
|
return false; |
|
} |
|
})(); |