// ==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 3.7.1 // @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.21.0/dist/exif-reader.min.js // @require https://cdn.jsdelivr.net/npm/pako@2.1.0/dist/pako.min.js // ==/UserScript== (async function () { const STRICT_CHECK = false || JSON.parse(localStorage['CATBOX_HDG_STRICT_CHECK'] || 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\.catbox\.moe\/([a-z0-9]{6}\.(?:png|jpg|webp))$/i; const RE_CATBOX_FILENAME = /^catbox_[a-z0-9]{6}\.(?:png|jpg|webp)$/i; const RE_NAI_FILENAME = /^.*s\-\d+\s*(?:\(\d+\))?(\.png)+\s*$/; let loaded = false; let thread_match = false; let qr_updated = false; const posts = new Set(); function log(msg) { console.log(`[4chan /sdg/ catbox.moe userscript] ${msg}`); } 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); } }); }); } 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) { 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); 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'); const filenameInput = document.querySelector('#qr-filename'); const fileEvent = new CustomEvent('QRSetFile', { detail: { file: file } }); document.dispatchEvent(fileEvent); filenameInput.value = `catbox_${filenameMatch[1]}`; 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'); if (hostedOn4chan) { const res = await get(href); if (res.status != 200) { return; } const image = await loadImage(res.response); metadata = readInfoFromImageStealth(image); if (metadata) { try { let naiMetadata = JSON.parse(metadata); metadata = parseNaiMetadata(naiMetadata['Comment']); } catch { ; } } } 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')) { 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')) { let rawWebpMetadata = chunksToArray(chunks)?.buffer; rawWebpMetadata = ExifReader.load(rawWebpMetadata); if (Object.keys(rawWebpMetadata).includes('UserComment')) { const webpMetadata = String.fromCharCode(...rawWebpMetadata.UserComment.value.slice(9).filter((value) => value !== 0)); updatePromptBox(box, webpMetadata); } 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)); 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; } const href = `https://files.catbox.moe/${name}`; log(`parsed catbox link: ${href}`); link.href = href; updateLinkHover(link); } async function updateDownloadLinks(root=null, limit=100000) { 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, ''); } const downloadLinks = Array.from(root.querySelectorAll('.file-info a[href*="4cdn.org"][download^="catbox_" i]')).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('a[href*=".catbox.moe/"]:is([href$=".png" i], [href$=".jpg" i], [href$=".webp" i])')).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 = 'white'; box.style.backgroundColor = 'rgba(0,0,255,0.2)'; 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;'); 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'); 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) { return; } evt.preventDefault(); uploadToCatbox(evt.dataTransfer.files[0]); }); 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) { if (data.data && data.name) { data = data.data } let naming = true let text = '' let name = '' for (let i = 0; i < data.length; i++) { let code = data[i] if (naming) { if (code) { name += String.fromCharCode(code) } else { naming = false } } else { if (code) { text += String.fromCharCode(code) } } } return { keyword: name, text: text } } /** * 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': if (!result.tEXt) { result.tEXt = {}; } let textChunk = textDecode(chunk.data); 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; } })();