// ==UserScript== // @name Soybooru Steganography Tool // @namespace http://tampermonkey.net/ // @version 2.8 // @description Adds a compact LSB steganography scanner below images within #image-list and #Imagemain sections on soybooru.com. // @author Chud // @match https://soybooru.com/* // @grant GM_addStyle // @run-at document-idle // ==/UserScript== // ARTIFACTS DO NOT MEAN SOMETHING IS EMBEDDED // THIS DOES NOT WORK WITH THUMBNAILS, THE FULL IMAGE MUST BE VIEWED FIRST (function() { 'use strict'; // --- STYLES --- GM_addStyle(` /* Wrapper for image + controls unit for *thumbnail* views */ .steg-image-wrapper { display: inline-block; /* Allows wrappers to sit side-by-side */ vertical-align: top; /* Aligns them at the top */ margin: 0 5px 25px 0; /* Add margin around each image+control block */ box-sizing: border-box; /* Optional: Add a border or background to see the wrapper clearly */ /* border: 1px dashed blue; */ } /* Controls container common styles */ .steg-controls-container { background: #f0f0f0; border: 1px solid #ccc; border-top: none; padding: 5px; margin-top: 5px; /* Space between image and controls */ border-bottom-left-radius: 5px; border-bottom-right-radius: 5px; display: block; /* Always block to force new line for controls */ box-sizing: border-box; width: 100%; /* Take full width of its parent wrapper or main image */ text-align: center; } /* Specific styles for controls under the main (large) image */ #Imagemain .steg-controls-container { /* For single full-size images, ensure it's centered and has appropriate width */ width: fit-content; /* Allow width to match content */ min-width: 250px; /* Wider for full-size view */ max-width: 400px; /* Limit max width to avoid stretching too much */ margin-left: auto; /* Center the controls */ margin-right: auto; margin-bottom: 25px; /* More space below the large image's controls */ } .steg-controls-container p { margin: 0 0 5px 0; font-size: 11px; font-weight: bold; color: #333; text-align: center; } .steg-slider { width: 100%; margin: 0; display: block; } #steg-toast-notification { position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); background-color: rgba(0, 0, 0, 0.75); color: white; padding: 10px 20px; border-radius: 5px; z-index: 99999; opacity: 0; transition: opacity 0.5s; pointer-events: none; } `); // --- NOTIFICATION --- const toast = document.createElement('div'); toast.id = 'steg-toast-notification'; document.body.appendChild(toast); /** * Displays a short-lived notification message to the user. * @param {string} message - The message to display. * @param {number} duration - How long to display the message in milliseconds. */ function showNotification(message, duration = 3000) { toast.textContent = message; toast.style.opacity = '1'; setTimeout(() => { toast.style.opacity = '0'; }, duration); } // --- CORE LOGIC (decodeMessageFromImage is no longer used for automatic copy) --- /** * Reveals data hidden in the least significant bits of an image. * @param {ImageData} imageData - The pixel data from a canvas. * @param {number} bits - The number of LSBs to reveal (0-7). */ function doUnhideImage(imageData, bits) { const pixels = imageData.data; const shift = 8 - bits; for (let i = 0; i < pixels.length; i += 4) { pixels[i] = (pixels[i] % (1 << bits)) << shift; // Red pixels[i + 1] = (pixels[i + 1] % (1 << bits)) << shift; // Green pixels[i + 2] = (pixels[i + 2] % (1 << bits)) << shift; // Blue // Alpha channel (pixels[i + 3]) is left untouched. } } /** * Decodes a message hidden in the LSB of an image's RGB channels. * NOTE: This function is now only for potential future use or manual invocation. * It is no longer triggered automatically by the slider. * @param {string} imageUrl - The URL of the image to decode. * @returns {Promise} A promise that resolves with the decoded message. */ function decodeMessageFromImage(imageUrl) { return new Promise((resolve, reject) => { const img = new Image(); img.crossOrigin = "Anonymous"; img.onload = () => { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); canvas.width = img.width; canvas.height = img.height; ctx.drawImage(img, 0, 0); const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const pixel = imageData.data; let binaryMessage = ''; for (let i = 0; i < pixel.length; i += 4) { binaryMessage += (pixel[i] & 1); binaryMessage += (pixel[i + 1] & 1); binaryMessage += (pixel[i + 2] & 1); } let output = ""; for (let i = 0; i < binaryMessage.length; i += 8) { const byteString = binaryMessage.substr(i, 8); if (byteString.length !== 8) break; const charCode = parseInt(byteString, 2); if (charCode === 0) break; output += String.fromCharCode(charCode); } resolve(output); }; img.onerror = () => reject("Error: Could not load image for decoding."); img.src = imageUrl; }); } /** * Adds steganography controls below a given image element. * @param {HTMLImageElement} imgElement - The image element to add controls for. * @param {boolean} isMainImage - True if this is the full-size image in #Imagemain. */ function addControlsToImage(imgElement, isMainImage = false) { // Find the outermost container that wraps the image and acts as a unit let imageUnitElement = imgElement; // Prioritize a thumbnail container if available (for #image-list) if (!isMainImage) { // Only look for .thumb if it's not the main image const thumbContainer = imgElement.closest('.thumb'); if (thumbContainer) { imageUnitElement = thumbContainer; } else { // If not a .thumb, check if it's directly inside a link const parentAnchor = imgElement.closest('a'); if (parentAnchor) { imageUnitElement = parentAnchor; } } } else { // For #Imagemain, the image itself or its direct link/wrapper is usually the unit. // Avoid creating unnecessary layers. const parentAnchor = imgElement.closest('a'); if (parentAnchor) { imageUnitElement = parentAnchor; } } // Prevent adding controls multiple times if (imageUnitElement.dataset.stegProcessed) { return; } imageUnitElement.dataset.stegProcessed = 'true'; // Mark the *unit element* as processed const fullImageUrl = (imgElement.srcset && typeof imgElement.srcset === 'string') ? imgElement.srcset.split(',').pop().trim().split(' ')[0] : imgElement.src; if (!fullImageUrl || fullImageUrl.startsWith('data:')) { return; } if (!imgElement.dataset.originalSrc) { imgElement.dataset.originalSrc = imgElement.src; } const controlsContainer = document.createElement('div'); controlsContainer.className = 'steg-controls-container'; controlsContainer.innerHTML = `

LSB Scan

`; if (isMainImage) { // For the main image, insert controls directly after the image unit element imageUnitElement.parentNode.insertBefore(controlsContainer, imageUnitElement.nextSibling); // No wrapping needed, as it's typically a single large image in a block context. // The specific CSS for #Imagemain .steg-controls-container will handle its sizing/centering. } else { // For thumbnail images, use the wrapper approach const stegImageWrapper = document.createElement('div'); stegImageWrapper.className = 'steg-image-wrapper'; imageUnitElement.parentNode.insertBefore(stegImageWrapper, imageUnitElement); stegImageWrapper.appendChild(imageUnitElement); stegImageWrapper.appendChild(controlsContainer); } const slider = controlsContainer.querySelector('.steg-slider'); // Prevent clicks on slider from propagating to underlying links controlsContainer.addEventListener('click', (e) => { e.stopPropagation(); }); slider.addEventListener('input', async (e) => { const bits = parseInt(e.target.value, 10); // Restore original image if slider is at 0 if (bits === 0) { imgElement.src = imgElement.dataset.originalSrc; return; } // Always perform visual LSB revelation const tempImg = new Image(); tempImg.crossOrigin = "Anonymous"; tempImg.onload = () => { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); canvas.width = tempImg.width; canvas.height = tempImg.height; ctx.drawImage(tempImg, 0, 0); const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); doUnhideImage(imageData, bits); ctx.putImageData(imageData, 0, 0); imgElement.src = canvas.toDataURL(); }; tempImg.onerror = () => showNotification("Error loading image for LSB view."); tempImg.src = fullImageUrl; }); } // --- INITIALIZATION & OBSERVER --- function initializeSteganographyTool() { const imageListSection = document.getElementById('image-list'); const imageMainSection = document.getElementById('Imagemain'); // Get the main image section if (imageListSection) { // Process images in #image-list const observerImageList = new MutationObserver((mutationsList) => { for (const mutation of mutationsList) { if (mutation.type === 'childList') { mutation.addedNodes.forEach(node => { if (node.nodeType === 1) { if (node.tagName === 'IMG' && node.src && node.width > 20 && node.height > 20 && imageListSection.contains(node)) { addControlsToImage(node, false); // Pass isMainImage = false } node.querySelectorAll('img').forEach(img => { if (img.src && img.width > 20 && img.height > 20 && imageListSection.contains(img)) { addControlsToImage(img, false); // Pass isMainImage = false } }); } }); } } }); observerImageList.observe(imageListSection, { childList: true, subtree: true }); imageListSection.querySelectorAll('.thumb img').forEach(img => { if (img.src && img.width > 20 && img.height > 20) { addControlsToImage(img, false); // Pass isMainImage = false } }); imageListSection.querySelectorAll('img:not(.thumb img)').forEach(img => { if (img.src && img.width > 20 && img.height > 20) { addControlsToImage(img, false); // Pass isMainImage = false } }); } else { console.log("Soybooru Steganography Tool: #image-list section not found."); } if (imageMainSection) { // Process the main image in #Imagemain // The main image is typically already present on load, but observer ensures dynamic changes are caught const mainImage = imageMainSection.querySelector('img'); if (mainImage && mainImage.src) { addControlsToImage(mainImage, true); // Pass isMainImage = true } const observerImageMain = new MutationObserver((mutationsList) => { for (const mutation of mutationsList) { if (mutation.type === 'childList') { mutation.addedNodes.forEach(node => { if (node.nodeType === 1) { if (node.tagName === 'IMG' && node.src && imageMainSection.contains(node)) { addControlsToImage(node, true); // Pass isMainImage = true } node.querySelectorAll('img').forEach(img => { if (img.src && imageMainSection.contains(img)) { addControlsToImage(img, true); // Pass isMainImage = true } }); } }); } } }); // Observe the main image container for changes observerImageMain.observe(imageMainSection, { childList: true, subtree: true }); } else { console.log("Soybooru Steganography Tool: #Imagemain section not found."); } } // Run initialization initializeSteganographyTool(); })();