// ==UserScript== // @name Download Images from JupyterLab // @namespace https://github.com/alberti42/ // @match http://*/lab/* // @match https://*/lab/* // @grant none // @version 1.4 // @author Andrea Alberti // @description Handle image and SVG downloads in JupyterLab // ==/UserScript== const RETRY_INTERVAL_MS = 500; // Time between retries in milliseconds const MAX_RETRIES = 40; // Maximum number of retries // Function to create and append a button function addDownloadButton(divElement) { if (divElement.querySelector('.download-image-button')) { return; // Avoid duplicates } // Ensure the parent div has `position: relative` for absolute positioning of the button divElement.style.position = 'relative'; // Retrieve the element and its src attribute const imgElement = divElement.querySelector('img'); if (!imgElement || !imgElement.src) { console.warn('No element with a valid src found inside this div.'); return; } // Determine MIME type from the `data-mime-type` attribute const mimeType = divElement.getAttribute('data-mime-type'); if (!mimeType) { console.warn('No data-mime-type attribute found on this div.'); return; } // Create the button element const button = document.createElement('button'); button.className = 'download-image-button'; button.style.position = 'absolute'; // Absolute positioning button.style.top = '5px'; // Place near the top button.style.right = '5px'; // Place it on the right border button.style.border = 'none'; // Optional: Remove border for a cleaner look button.style.backgroundColor = 'transparent'; // Optional: Transparent background button.style.padding = '5px'; // Optional: Add some padding for a better click target button.style.cursor = 'pointer'; // Show pointer cursor on hover // Add a Font Awesome icon to the button const icon = document.createElement('i'); icon.className = 'fas fa-download'; // Font Awesome class for a download icon icon.style.fontSize = '1.2em'; // Adjust icon size icon.style.color = '#007bff'; // Optional: Add a color to the icon button.appendChild(icon); // Attach an event listener to the button button.addEventListener('click', () => { // Check if the data is base64-encoded const isBase64 = imgElement.src.startsWith(`data:${mimeType};base64,`); // Check if the data is percent-encoded const isPercentEncoded = imgElement.src.startsWith(`data:${mimeType},`); if (isPercentEncoded || isBase64) { // Extract the content after the comma const content = imgElement.src.split(',')[1]; // Decode the content based on the encoding type const decodedContent = isBase64 ? atob(content) : decodeURIComponent(content); // Log the decoded content (for debugging purposes) console.log(decodedContent); // Create a Blob for the decoded content const blob = new Blob( isBase64 ? [Uint8Array.from(decodedContent, char => char.charCodeAt(0))] : [decodedContent], { type: mimeType } ); // Create a temporary link element const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = `plot.${mimeType.split('/')[1]}`; // Use MIME type to determine file extension document.body.appendChild(link); // Append to body for the click event link.click(); // Trigger the download document.body.removeChild(link); // Clean up } else { console.error(`Image source does not match expected MIME type or encoding: ${mimeType}`); } }); // Append the button to the div divElement.appendChild(button); } // Function to process all existing image and SVG elements function processExistingElements() { const divElements = document.querySelectorAll('.jp-RenderedSVG, .jp-RenderedImage'); divElements.forEach(divElement => addDownloadButton(divElement)); } // Mutation Observer callback function observeElements(mutationsList) { for (const mutation of mutationsList) { if (mutation.type === 'childList') { mutation.addedNodes.forEach(node => { // Check the descendants of the node for image and SVG containers if (node.querySelectorAll) { const divElements = node.querySelectorAll('.jp-RenderedSVG, .jp-RenderedImage'); divElements.forEach(divElement => addDownloadButton(divElement)); } }); } } } // Function to set up the observer function setupObserver(notebookContainer) { console.log('Setting up observer...'); const observer = new MutationObserver(observeElements); observer.observe(notebookContainer, { childList: true, subtree: true }); console.log('Observer set up successfully'); } // Function to retry finding the notebook container function waitForNotebookContainer(retriesLeft) { const notebookContainer = document.querySelector('.jp-Notebook'); if (notebookContainer) { processExistingElements(); setupObserver(notebookContainer); } else if (retriesLeft > 0) { console.log(`Retrying... (${retriesLeft} retries left)`); setTimeout(() => waitForNotebookContainer(retriesLeft - 1), RETRY_INTERVAL_MS); } else { console.error('Notebook container not found after maximum retries'); } } // Initialize the script function initialize() { console.log('Initializing script...'); waitForNotebookContainer(MAX_RETRIES); } // Run the initialization when the DOM is fully loaded window.addEventListener('load', initialize);