├── screenshots ├── .gitkeep ├── menu.jpg ├── options.jpg └── popup.jpg ├── icons ├── icon128.png ├── icon16.png └── icon48.png ├── LICENSE.txt ├── manifest.json ├── options.js ├── options.html ├── popup.html ├── content.js ├── popup.js ├── README.md ├── background.js └── exif.js /screenshots/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /icons/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/digitalego-one/ExcaliburAI-Civitai-Prompt-Extractor/HEAD/icons/icon128.png -------------------------------------------------------------------------------- /icons/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/digitalego-one/ExcaliburAI-Civitai-Prompt-Extractor/HEAD/icons/icon16.png -------------------------------------------------------------------------------- /icons/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/digitalego-one/ExcaliburAI-Civitai-Prompt-Extractor/HEAD/icons/icon48.png -------------------------------------------------------------------------------- /screenshots/menu.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/digitalego-one/ExcaliburAI-Civitai-Prompt-Extractor/HEAD/screenshots/menu.jpg -------------------------------------------------------------------------------- /screenshots/options.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/digitalego-one/ExcaliburAI-Civitai-Prompt-Extractor/HEAD/screenshots/options.jpg -------------------------------------------------------------------------------- /screenshots/popup.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/digitalego-one/ExcaliburAI-Civitai-Prompt-Extractor/HEAD/screenshots/popup.jpg -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Proprietary License 2 | 3 | Copyright (c) 2024 Vadim Sokolovsky | Digital Ego One 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to use 7 | the Software for personal, non-commercial purposes only, subject to the following conditions: 8 | 9 | 1. The Software, or any derivative works from it, is intended for personal use only and may not 10 | be distributed, in original or modified form, for free or for a fee, without 11 | explicit written permission from the copyright holder. 12 | 2. The Software is provided "as is", without warranty of any kind, express or implied, including 13 | but not limited to the warranties of merchantability, fitness for a particular purpose, and 14 | noninfringement. In no event shall the authors or copyright holders be liable for any claim, 15 | damages, or other liability, whether in an action of contract, tort, or otherwise, arising 16 | from, out of, or in connection with the Software or the use or other dealings in the Software. 17 | 18 | By using the Software, you agree to these terms. 19 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "ExcaliburAI - Civitai Prompt Extractor", 4 | "version": "1.2", 5 | "description": "Copies prompt and metadata in clipboard with 1 click from Ai-generated images, if there is any.", 6 | "permissions": [ 7 | "contextMenus", 8 | "scripting", 9 | "clipboardWrite", 10 | "notifications", 11 | "storage" 12 | ], 13 | "host_permissions": [ 14 | "https://civitai.com/*" 15 | ], 16 | "background": { 17 | "service_worker": "background.js" 18 | }, 19 | "content_scripts": [ 20 | { 21 | "matches": [ 22 | "https://civitai.com/*" 23 | ], 24 | "js": ["exif.js", "content.js"], 25 | "all_frames": true, 26 | "run_at": "document_idle" 27 | } 28 | ], 29 | "action": { 30 | "default_popup": "popup.html", 31 | "default_icon": { 32 | "16": "icons/icon16.png", 33 | "48": "icons/icon48.png", 34 | "128": "icons/icon128.png" 35 | } 36 | }, 37 | "options_ui": { 38 | "page": "options.html", 39 | "open_in_tab": true 40 | }, 41 | "icons": { 42 | "16": "icons/icon16.png", 43 | "48": "icons/icon48.png", 44 | "128": "icons/icon128.png" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /options.js: -------------------------------------------------------------------------------- 1 | // options.js 2 | 3 | document.addEventListener('DOMContentLoaded', () => { 4 | const enableNotificationsCheckbox = document.getElementById('enableNotifications'); 5 | const allowedDomainsInput = document.getElementById('allowedDomains'); 6 | const saveButton = document.getElementById('saveButton'); 7 | 8 | // Load existing settings 9 | chrome.storage.local.get(['enableNotifications', 'allowedDomains'], (result) => { 10 | enableNotificationsCheckbox.checked = result.enableNotifications !== false; // Default to true 11 | if (result.allowedDomains && result.allowedDomains.length > 0) { 12 | allowedDomainsInput.value = result.allowedDomains.join(', '); 13 | } 14 | }); 15 | 16 | // Save settings when the save button is clicked 17 | saveButton.addEventListener('click', () => { 18 | const enableNotifications = enableNotificationsCheckbox.checked; 19 | const allowedDomains = allowedDomainsInput.value.split(',').map(domain => domain.trim()).filter(domain => domain !== ''); 20 | 21 | chrome.storage.local.set({ enableNotifications, allowedDomains }, () => { 22 | alert('Settings saved successfully!'); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Copy Prompt If Any - Options 6 | 36 | 37 | 38 |

Extension Options

39 |
40 | 44 |
45 |
46 | 47 | 48 |
49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Last Copied Prompt 7 | 48 | 49 | 50 |

Last Copied Prompt

51 | 52 | 53 |
54 |

Prompt

55 |
No prompt copied yet.
56 | 57 |
58 | 59 | 60 |
61 |

Negative Prompt

62 |
No negative prompt copied yet.
63 | 64 |
65 | 66 | 67 |
68 |

Other Metadata

69 |
No metadata copied yet.
70 | 71 |
72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /content.js: -------------------------------------------------------------------------------- 1 | // content.js 2 | 3 | // Listen for messages from background script 4 | chrome.runtime.onMessage.addListener(async (request, sender, sendResponse) => { 5 | if (request.action === "processImage" && request.buffer) { 6 | try { 7 | console.log("Received image buffer"); 8 | 9 | // Parse EXIF data 10 | const exifData = EXIF.readFromBinaryFile(request.buffer); 11 | const userComment = exifData.UserComment || exifData.userComment; 12 | 13 | console.log("Extracted userComment:", userComment); 14 | console.log("UserComment length:", userComment.length); 15 | console.log("UserComment content:", userComment); 16 | 17 | if (userComment && userComment.length > 0) { 18 | const decodedText = decodeUserComment(userComment); 19 | console.log("Decoded userComment:", decodedText); 20 | 21 | if (decodedText && decodedText.trim() !== "" && decodedText !== "UNICODE") { 22 | // Write to clipboard 23 | await copyToClipboard(decodedText); 24 | console.log("Copied to clipboard successfully"); 25 | 26 | // Send notification 27 | chrome.runtime.sendMessage({ type: "notification", text: "Copied to clipboard" }); 28 | } else if (decodedText === "UNICODE") { 29 | // Send notification: No prompts found 30 | chrome.runtime.sendMessage({ type: "notification", text: "No prompts found" }); 31 | } else { 32 | // Send notification: Failed to decode 33 | chrome.runtime.sendMessage({ type: "notification", text: "Failed to decode the prompt" }); 34 | } 35 | } else { 36 | // Send notification: No prompts found 37 | chrome.runtime.sendMessage({ type: "notification", text: "No prompts found" }); 38 | } 39 | } catch (error) { 40 | console.error('Error processing image:', error); 41 | // Send notification: An error occurred 42 | chrome.runtime.sendMessage({ type: "notification", text: "An error occurred while processing the image." }); 43 | } 44 | } 45 | }); 46 | 47 | // Function to decode the userComment field 48 | function decodeUserComment(userComment) { 49 | if (!userComment) return ''; 50 | 51 | let decoded = ''; 52 | 53 | if (typeof userComment === 'string') { 54 | // Define the prefix based on EXIF specification 55 | const unicodePrefix = 'UNICODE\0\0 '; 56 | const asciiPrefix = 'ASCII\0\0 '; 57 | 58 | if (userComment.startsWith(unicodePrefix)) { 59 | // Remove the 'UNICODE\0\0 ' prefix 60 | const unicodeStr = userComment.slice(unicodePrefix.length).trim(); 61 | decoded = unicodeStr; 62 | } else if (userComment.startsWith(asciiPrefix)) { 63 | // Remove the 'ASCII\0\0 ' prefix 64 | const asciiStr = userComment.slice(asciiPrefix.length).trim(); 65 | decoded = asciiStr; 66 | } else { 67 | // No prefix, return as is 68 | decoded = userComment.trim(); 69 | } 70 | } 71 | 72 | return decoded; 73 | } 74 | 75 | // Function to copy text to the clipboard using a temporary textarea 76 | async function copyToClipboard(text) { 77 | return new Promise((resolve, reject) => { 78 | try { 79 | // Create a temporary textarea element 80 | const textarea = document.createElement('textarea'); 81 | textarea.value = text; 82 | 83 | // Make the textarea out of viewport 84 | textarea.style.position = 'fixed'; 85 | textarea.style.top = '-9999px'; 86 | textarea.style.left = '-9999px'; 87 | document.body.appendChild(textarea); 88 | 89 | // Select the text 90 | textarea.focus(); 91 | textarea.select(); 92 | 93 | // Execute the copy command 94 | const successful = document.execCommand('copy'); 95 | if (successful) { 96 | resolve(); 97 | } else { 98 | reject(new Error('Copy command was unsuccessful')); 99 | } 100 | 101 | // Remove the textarea 102 | document.body.removeChild(textarea); 103 | } catch (err) { 104 | console.error('Failed to copy:', err); 105 | reject(err); 106 | } 107 | }); 108 | } 109 | -------------------------------------------------------------------------------- /popup.js: -------------------------------------------------------------------------------- 1 | // popup.js 2 | 3 | document.addEventListener('DOMContentLoaded', () => { 4 | const promptDiv = document.getElementById('prompt'); 5 | const negativePromptDiv = document.getElementById('negativePrompt'); 6 | const otherMetadataDiv = document.getElementById('otherMetadata'); 7 | 8 | const copyPromptButton = document.getElementById('copyPromptButton'); 9 | const copyNegativePromptButton = document.getElementById('copyNegativePromptButton'); 10 | const copyMetadataButton = document.getElementById('copyMetadataButton'); 11 | 12 | // Retrieve the last copied prompt from storage 13 | chrome.storage.local.get(['lastCopiedPrompt'], (result) => { 14 | if (result.lastCopiedPrompt) { 15 | const sanitizedText = sanitizeText(result.lastCopiedPrompt); 16 | const parsedData = parsePromptData(sanitizedText); 17 | 18 | // Display the parsed sections 19 | promptDiv.textContent = parsedData.prompt || 'No prompt found.'; 20 | negativePromptDiv.textContent = parsedData.negativePrompt || 'No negative prompt found.'; 21 | otherMetadataDiv.textContent = parsedData.otherMetadata || 'No metadata found.'; 22 | } else { 23 | promptDiv.textContent = 'No prompt copied yet.'; 24 | negativePromptDiv.textContent = 'No negative prompt copied yet.'; 25 | otherMetadataDiv.textContent = 'No metadata copied yet.'; 26 | } 27 | }); 28 | 29 | // Event listeners for Copy buttons 30 | copyPromptButton.addEventListener('click', () => { 31 | const text = promptDiv.textContent; 32 | if (text && text !== 'No prompt found.') { 33 | copyTextToClipboard(text) 34 | .then(() => { 35 | alert('Prompt copied to clipboard.'); 36 | }) 37 | .catch((err) => { 38 | console.error('Failed to copy Prompt:', err); 39 | alert('Failed to copy Prompt.'); 40 | }); 41 | } else { 42 | alert('No prompt to copy.'); 43 | } 44 | }); 45 | 46 | copyNegativePromptButton.addEventListener('click', () => { 47 | const text = negativePromptDiv.textContent; 48 | if (text && text !== 'No negative prompt found.') { 49 | copyTextToClipboard(text) 50 | .then(() => { 51 | alert('Negative Prompt copied to clipboard.'); 52 | }) 53 | .catch((err) => { 54 | console.error('Failed to copy Negative Prompt:', err); 55 | alert('Failed to copy Negative Prompt.'); 56 | }); 57 | } else { 58 | alert('No negative prompt to copy.'); 59 | } 60 | }); 61 | 62 | copyMetadataButton.addEventListener('click', () => { 63 | const text = otherMetadataDiv.textContent; 64 | if (text && text !== 'No metadata found.') { 65 | copyTextToClipboard(text) 66 | .then(() => { 67 | alert('Metadata copied to clipboard.'); 68 | }) 69 | .catch((err) => { 70 | console.error('Failed to copy Metadata:', err); 71 | alert('Failed to copy Metadata.'); 72 | }); 73 | } else { 74 | alert('No metadata to copy.'); 75 | } 76 | }); 77 | }); 78 | 79 | // Function to sanitize text by removing or replacing non-printable and control characters 80 | function sanitizeText(text) { 81 | // Replace zero-width spaces with regular spaces and remove other control characters 82 | return text 83 | .replace(/\u200B/g, ' ') // Replace zero-width space with space 84 | .replace(/[\u0000-\u001F\u007F-\u009F]/g, ''); // Remove other control characters 85 | } 86 | 87 | // Function to parse the prompt data into sections 88 | function parsePromptData(text) { 89 | const result = { 90 | prompt: '', 91 | negativePrompt: '', 92 | otherMetadata: '' 93 | }; 94 | 95 | // Use case-insensitive regex to find sections 96 | const unicodeRegex = /UNICODE\s*(.*?)\s*Negative prompt:/i; 97 | const negativePromptRegex = /Negative prompt:\s*(.*?)\s*Steps:/i; 98 | const otherMetadataRegex = /Steps:\s*(.*)/i; 99 | 100 | const unicodeMatch = text.match(unicodeRegex); 101 | if (unicodeMatch && unicodeMatch[1]) { 102 | result.prompt = unicodeMatch[1].trim(); 103 | } 104 | 105 | const negativePromptMatch = text.match(negativePromptRegex); 106 | if (negativePromptMatch && negativePromptMatch[1]) { 107 | result.negativePrompt = negativePromptMatch[1].trim(); 108 | } 109 | 110 | const otherMetadataMatch = text.match(otherMetadataRegex); 111 | if (otherMetadataMatch && otherMetadataMatch[1]) { 112 | result.otherMetadata = otherMetadataMatch[1].trim(); 113 | } 114 | 115 | return result; 116 | } 117 | 118 | // Function to copy text to the clipboard using the Clipboard API 119 | async function copyTextToClipboard(text) { 120 | if (!navigator.clipboard) { 121 | // Fallback method using a temporary textarea 122 | return new Promise((resolve, reject) => { 123 | try { 124 | const textarea = document.createElement('textarea'); 125 | textarea.value = text; 126 | textarea.style.position = 'fixed'; 127 | textarea.style.top = '-9999px'; 128 | textarea.style.left = '-9999px'; 129 | document.body.appendChild(textarea); 130 | textarea.focus(); 131 | textarea.select(); 132 | const successful = document.execCommand('copy'); 133 | document.body.removeChild(textarea); 134 | if (successful) { 135 | resolve(); 136 | } else { 137 | reject(new Error('Copy command was unsuccessful')); 138 | } 139 | } catch (err) { 140 | reject(err); 141 | } 142 | }); 143 | } else { 144 | return navigator.clipboard.writeText(text); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ExcaliburAI - Civitai Prompt Extractor [v1.2.0] 2 | 3 | ![ExcaliburAI Logo](./icons/icon128.png) 4 | 5 | **ExcaliburAI - Civitai Prompt Extractor** is a powerful Chrome extension designed for AI enthusiasts, digital artists, and creators. It effortlessly extracts and organizes essential prompt data directly from AI-generated images' EXIF metadata, allowing you to manage and utilize your prompts with ease. 6 | 7 | ## 📄 Table of Contents 8 | 9 | - [🎉 Now Live on Chrome Web Store!](#-now-live-on-chrome-web-store) 10 | - [🚀 Support us on Product Hunt](#-support-us-on-product-hunt) 11 | - [📖 Features](#-features) 12 | - [🚀 Installation](#-installation) 13 | - [🖥️ Usage](#️-usage) 14 | - [⚙️ Configuration](#️-configuration) 15 | - [📸 Screenshots](#-screenshots) 16 | - [🔧 Development](#-development) 17 | - [🤝 Contributing](#-contributing) 18 | - [📚 Credits](#-credits) 19 | - [📜 License](#-license) 20 | - [📞 Support](#-support) 21 | 22 | ## 🎉 Now Live on Chrome Web Store! 23 | 24 | Excited to use **ExcaliburAI - Civitai Prompt Extractor**? You can install it directly from the [Chrome Web Store](https://chromewebstore.google.com/detail/excaliburai-civitai-promp/jdkgelpgnofafbgbbmlgngehmlkllaah). 25 | 26 | ## 🚀 Support us on Product Hunt 27 | 28 | We recently launched on [Product Hunt](https://www.producthunt.com/posts/excaliburai-civitai-prompt-extractor). Your upvote will make a significant difference and help us reach a wider audience. Check it out and support us by leaving an upvote! 29 | 30 | ## 📖 Features 31 | 32 | - **🖼️ Context Menu Integration**: Right-click on any AI-generated image and select **"Copy Prompt If Any"** to instantly extract prompt data. 33 | - **🖥️ Interactive Popup Interface**: 34 | - **🔍 Organized Sections**: View **Prompt**, **Negative Prompt**, and **Other Metadata** in clearly labeled sections. 35 | - **📋 Individual Copy Buttons**: Copy each section separately with dedicated **Copy** buttons. 36 | - **💡 Responsive Design**: Clean layout that adapts to various screen sizes without horizontal scrolling. 37 | - **🔒 Secure Handling**: All data is processed locally within your browser, ensuring privacy and security. 38 | - **⚡ Quick Access**: Instantly access the latest copied information directly from the popup. 39 | - **✂️ Intelligent Parsing**: Automatically parses and formats EXIF data to accurately separate prompts and metadata. 40 | - **⚙️ Customizable Settings**: 41 | - **Enable/Disable Notifications**: Choose whether to receive alerts upon successful or failed copy actions. 42 | - **Specify Allowed Domains**: Restrict the extension's functionality to specific websites for enhanced security. 43 | - **🚀 In-Memory Caching**: Optimizes performance by caching EXIF data during your browsing session. 44 | - **🔒 Privacy-Focused**: Operates entirely within your browser without transmitting any data externally. 45 | 46 | ## 🚀 Installation 47 | 48 | 1. **Clone the Repository**: 49 | ```bash 50 | git clone https://github.com/digitalego-one/ExcaliburAI-Civitai-Prompt-Extractor 51 | ``` 52 | 2. **Navigate to the Directory**: 53 | ```bash 54 | cd ExcaliburAI-Civitai-Prompt-Extractor 55 | ``` 56 | 3. **Load the Extension in Chrome**: 57 | - Open Chrome and navigate to `chrome://extensions/`. 58 | - Enable **Developer mode** by toggling the switch in the top right corner. 59 | - Click on **Load unpacked** and select the cloned repository folder. 60 | 61 | ## 🖥️ Usage 62 | 63 | 1. **Extracting Prompts**: 64 | - Navigate to any AI-generated image on supported websites. 65 | - **Right-click** on the image and select **"Copy Prompt If Any"** from the context menu. 66 | 67 | 2. **Viewing Extracted Data**: 68 | - Click on the **ExcaliburAI** toolbar icon to open the popup. 69 | - The popup displays the **Prompt**, **Negative Prompt**, and **Other Metadata** in separate sections. 70 | 71 | 3. **Copying Data**: 72 | - Use the **Copy** buttons next to each section to copy the respective data to your clipboard. 73 | 74 | ## ⚙️ Configuration 75 | 76 | Access the extension's settings to customize its behavior: 77 | 78 | 1. **Open Options Page**: 79 | - Click on the **ExcaliburAI** toolbar icon. 80 | - Click on the **Settings** or **Options** button within the popup, or navigate to `chrome://extensions/`, find **ExcaliburAI**, and click **Details** > **Extension options**. 81 | 82 | 2. **Configure Settings**: 83 | - **Enable/Disable Notifications**: Toggle to receive or suppress notifications. 84 | - **Specify Allowed Domains**: Enter domains where the extension is permitted to extract prompt data. 85 | 86 | ## 📸 Screenshots 87 | 88 | ![Context menu](./screenshots/menu.jpg) 89 | *Easy-to-find Context Menu Button* 90 | 91 | ![Popup Interface](./screenshots/popup.jpg) 92 | *Clean and organized popup displaying Prompt, Negative Prompt, and Other Metadata.* 93 | 94 | ![Options Page](./screenshots/options.jpg) 95 | *Settings page allowing customization of notifications and allowed domains.* 96 | 97 | ## 🔧 Development 98 | 99 | ### Prerequisites 100 | 101 | - **Node.js & npm**: Ensure you have Node.js and npm installed for managing dependencies (if any). 102 | 103 | ### Project Structure 104 | 105 | ``` 106 | ExcaliburAI-Prompt-Extractor/ 107 | │ 108 | ├── icons/ 109 | │ ├── icon16.png 110 | │ ├── icon48.png 111 | │ └── icon128.png 112 | │ 113 | ├── screenshots/ 114 | │ ├── menu.jpg 115 | │ ├── popup.jpg 116 | │ └── options.jpg 117 | │ 118 | ├── background.js 119 | ├── content.js 120 | ├── exif.js 121 | ├── popup.html 122 | ├── popup.js 123 | ├── options.html 124 | ├── options.js 125 | ├── manifest.json 126 | ├── README.md 127 | └── LICENSE 128 | ``` 129 | 130 | ### Building the Extension 131 | 132 | 1. **Install Dependencies**: 133 | ```bash 134 | npm install 135 | ``` 136 | 2. **Run Development Server** (if applicable): 137 | ```bash 138 | npm start 139 | ``` 140 | 3. **Package the Extension**: 141 | - Navigate to `chrome://extensions/`. 142 | - Click on **Pack extension**. 143 | - Select the extension directory and follow the prompts. 144 | 145 | ## 🤝 Contributing 146 | 147 | Contributions are welcome! Follow these steps to contribute to **ExcaliburAI - Civitai Prompt Extractor**: 148 | 149 | 1. **Fork the Repository**: 150 | - Click the **Fork** button at the top right of this page. 151 | 152 | 2. **Create a Feature Branch**: 153 | ```bash 154 | git checkout -b feature/YourFeatureName 155 | ``` 156 | 157 | 3. **Commit Your Changes**: 158 | ```bash 159 | git commit -m "Add your feature" 160 | ``` 161 | 162 | 4. **Push to the Branch**: 163 | ```bash 164 | git push origin feature/YourFeatureName 165 | ``` 166 | 167 | 5. **Open a Pull Request**: 168 | - Navigate to the original repository. 169 | - Click on **Compare & pull request**. 170 | - Provide a clear description of your changes and submit. 171 | 172 | ### Guidelines 173 | 174 | - **Code Quality**: Ensure your code follows best practices and is well-documented. 175 | - **Testing**: Test your changes thoroughly before submitting. 176 | - **Respect the Community**: Be respectful and constructive in your interactions. 177 | 178 | ## 📚 Credits 179 | 180 | **[exif-js](https://github.com/exif-js/exif-js)**: JavaScript library for reading EXIF data from images. 181 | 182 | ## 📜 License 183 | 184 | Distributed under the [Proprietary License](./LICENSE.txt) 185 | 186 | ## 📞 Support 187 | 188 | Have questions or need assistance? Reach out to us: 189 | 190 | - **Email**: [hello@digitalego.one](mailto:hello@digitalego.one) 191 | - **Issues**: [GitHub Issues](https://github.com/digitalego-one/ExcaliburAI-Civitai-Prompt-Extractor/issues) 192 | - **Website**: [https://www.excaliburai.top](https://excaliburai.top/) 193 | 194 | 195 | --- 196 | 197 | *Thank you for using **ExcaliburAI - Civitai Prompt Extractor**! We strive to continuously improve and provide the best experience for our users.* 198 | 199 | 200 | --- 201 | 202 | ![License](https://img.shields.io/badge/license-PROPRIETARY-blue.svg) 203 | ![Version](https://img.shields.io/badge/version-1.2.0-brightgreen.svg) 204 | 205 | 206 | -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | // background.js 2 | 3 | importScripts('exif.js'); 4 | 5 | // In-memory cache for EXIF data 6 | const exifCache = new Map(); 7 | 8 | // Create the context menu when the extension is installed 9 | chrome.runtime.onInstalled.addListener(() => { 10 | chrome.contextMenus.create({ 11 | id: "copyPrompt", 12 | title: "Copy prompt if any", 13 | contexts: ["image"] 14 | }); 15 | }); 16 | 17 | // Listen for context menu clicks 18 | chrome.contextMenus.onClicked.addListener(async (info, tab) => { 19 | if (info.menuItemId === "copyPrompt" && info.srcUrl && tab.id) { 20 | try { 21 | console.log("Attempting to fetch image:", info.srcUrl); 22 | 23 | // Retrieve settings 24 | const settings = await getSettings(); 25 | const { allowedDomains, enableNotifications } = settings; 26 | 27 | // Check if the image's domain is allowed 28 | if (allowedDomains.length > 0) { 29 | const imageUrl = new URL(info.srcUrl); 30 | if (!allowedDomains.includes(imageUrl.hostname)) { 31 | if (enableNotifications) { 32 | chrome.notifications.create({ 33 | type: 'basic', 34 | iconUrl: 'icons/icon48.png', 35 | title: 'Copy Prompt If Any', 36 | message: `Domain "${imageUrl.hostname}" is not allowed.` 37 | }); 38 | } 39 | return; 40 | } 41 | } 42 | 43 | let exifData; 44 | 45 | // Check if EXIF data is cached 46 | if (exifCache.has(info.srcUrl)) { 47 | console.log("Using cached EXIF data for:", info.srcUrl); 48 | exifData = exifCache.get(info.srcUrl); 49 | } else { 50 | // Fetch the image 51 | const response = await fetch(info.srcUrl); 52 | 53 | if (!response.ok) { 54 | throw new Error(`HTTP error! Status: ${response.status}`); 55 | } 56 | 57 | const arrayBuffer = await response.arrayBuffer(); 58 | console.log("ArrayBuffer fetched. Type:", Object.prototype.toString.call(arrayBuffer)); 59 | 60 | // Ensure arrayBuffer is an ArrayBuffer 61 | if (!(arrayBuffer instanceof ArrayBuffer)) { 62 | throw new TypeError('First argument to DataView constructor must be an ArrayBuffer'); 63 | } 64 | 65 | // Read EXIF data from the image 66 | exifData = EXIF.readFromBinaryFile(arrayBuffer); 67 | console.log("EXIF data:", exifData); 68 | 69 | // Cache the EXIF data 70 | exifCache.set(info.srcUrl, exifData); 71 | } 72 | 73 | const userComment = exifData.UserComment || exifData.userComment; 74 | console.log("UserComment:", userComment); 75 | 76 | if (userComment && userComment.length > 0) { 77 | const decodedText = decodeUserComment(userComment); 78 | console.log("Decoded userComment:", decodedText); 79 | 80 | if (decodedText && decodedText.trim() !== "" && decodedText !== "UNICODE") { 81 | // Store the last copied prompt in storage 82 | chrome.storage.local.set({ lastCopiedPrompt: decodedText }, () => { 83 | console.log('Last copied prompt stored:', decodedText); 84 | }); 85 | 86 | // Inject script to copy to clipboard 87 | await injectCopyScript(tab.id, decodedText); 88 | 89 | console.log("Injected copy script successfully."); 90 | 91 | // Show success notification if enabled 92 | if (enableNotifications) { 93 | chrome.notifications.create({ 94 | type: 'basic', 95 | iconUrl: 'icons/icon48.png', 96 | title: 'Copy Prompt If Any', 97 | message: 'Copied to clipboard' 98 | }); 99 | } 100 | } else if (decodedText === "UNICODE") { 101 | // Show notification: No prompts found 102 | if (enableNotifications) { 103 | chrome.notifications.create({ 104 | type: 'basic', 105 | iconUrl: 'icons/icon48.png', 106 | title: 'Copy Prompt If Any', 107 | message: 'No prompts found' 108 | }); 109 | } 110 | } else { 111 | // Show notification: Failed to decode 112 | if (enableNotifications) { 113 | chrome.notifications.create({ 114 | type: 'basic', 115 | iconUrl: 'icons/icon48.png', 116 | title: 'Copy Prompt If Any', 117 | message: 'Failed to decode the prompt' 118 | }); 119 | } 120 | } 121 | } else { 122 | // Show notification: No prompts found 123 | if (enableNotifications) { 124 | chrome.notifications.create({ 125 | type: 'basic', 126 | iconUrl: 'icons/icon48.png', 127 | title: 'Copy Prompt If Any', 128 | message: 'No prompts found' 129 | }); 130 | } 131 | } 132 | } catch (error) { 133 | console.error('Error processing image:', error); 134 | // Show error notification if enabled 135 | chrome.storage.local.get(['enableNotifications'], (result) => { 136 | if (result.enableNotifications !== false) { 137 | chrome.notifications.create({ 138 | type: 'basic', 139 | iconUrl: 'icons/icon48.png', 140 | title: 'Copy Prompt If Any', 141 | message: 'An error occurred while processing the image.' 142 | }); 143 | } 144 | }); 145 | } 146 | } 147 | }); 148 | 149 | // Function to decode the userComment field 150 | function decodeUserComment(userComment) { 151 | let decoded = ''; 152 | 153 | if (Array.isArray(userComment)) { 154 | // Convert to Uint8Array 155 | const uint8Array = new Uint8Array(userComment); 156 | 157 | // Decode the prefix (first 8 bytes) 158 | const prefix = new TextDecoder('ascii').decode(uint8Array.slice(0, 8)); 159 | 160 | if (prefix === 'UNICODE') { 161 | // Decode the rest as UTF-16LE 162 | const contentBytes = uint8Array.slice(8); 163 | decoded = new TextDecoder('utf-16le').decode(contentBytes); 164 | } else if (prefix === 'ASCII\0\0\0') { 165 | // Decode the rest as ASCII 166 | const contentBytes = uint8Array.slice(8); 167 | decoded = new TextDecoder('ascii').decode(contentBytes); 168 | } else { 169 | // No known prefix, attempt to decode as UTF-8 170 | decoded = new TextDecoder('utf-8').decode(uint8Array); 171 | } 172 | } else if (typeof userComment === 'string') { 173 | // Handle string directly 174 | const unicodePrefix = 'UNICODE'; 175 | const asciiPrefix = 'ASCII\0\0\0'; 176 | 177 | if (userComment.startsWith(unicodePrefix)) { 178 | decoded = userComment.slice(unicodePrefix.length).trim(); 179 | } else if (userComment.startsWith(asciiPrefix)) { 180 | decoded = userComment.slice(asciiPrefix.length).trim(); 181 | } else { 182 | decoded = userComment.trim(); 183 | } 184 | } 185 | 186 | return decoded; 187 | } 188 | 189 | // Function to inject a script that copies text to the clipboard 190 | async function injectCopyScript(tabId, text) { 191 | await chrome.scripting.executeScript({ 192 | target: { tabId: tabId }, 193 | func: copyTextToClipboard, 194 | args: [text], 195 | }); 196 | } 197 | 198 | // Function that runs in the context of the page to copy text to clipboard 199 | function copyTextToClipboard(text) { 200 | try { 201 | const textarea = document.createElement('textarea'); 202 | textarea.value = text; 203 | 204 | // Make the textarea out of viewport 205 | textarea.style.position = 'fixed'; 206 | textarea.style.top = '-9999px'; 207 | textarea.style.left = '-9999px'; 208 | textarea.style.width = '2em'; 209 | textarea.style.height = '2em'; 210 | textarea.style.padding = '0'; 211 | textarea.style.border = 'none'; 212 | textarea.style.outline = 'none'; 213 | textarea.style.boxShadow = 'none'; 214 | textarea.style.background = 'transparent'; 215 | document.body.appendChild(textarea); 216 | 217 | // Select the text 218 | textarea.focus(); 219 | textarea.select(); 220 | 221 | // Execute the copy command 222 | const successful = document.execCommand('copy'); 223 | if (successful) { 224 | console.log('Text copied to clipboard successfully.'); 225 | } else { 226 | console.error('Failed to copy text to clipboard.'); 227 | } 228 | 229 | // Remove the textarea 230 | document.body.removeChild(textarea); 231 | } catch (err) { 232 | console.error('Failed to copy:', err); 233 | } 234 | } 235 | 236 | // Function to get user settings 237 | function getSettings() { 238 | return new Promise((resolve, reject) => { 239 | chrome.storage.local.get(['enableNotifications', 'allowedDomains'], (result) => { 240 | if (chrome.runtime.lastError) { 241 | reject(chrome.runtime.lastError); 242 | } else { 243 | resolve({ 244 | enableNotifications: result.enableNotifications !== false, // Default to true 245 | allowedDomains: result.allowedDomains || [] 246 | }); 247 | } 248 | }); 249 | }); 250 | } 251 | -------------------------------------------------------------------------------- /exif.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | var debug = false; 4 | 5 | var root = this; 6 | 7 | var EXIF = function(obj) { 8 | if (obj instanceof EXIF) return obj; 9 | if (!(this instanceof EXIF)) return new EXIF(obj); 10 | this.EXIFwrapped = obj; 11 | }; 12 | 13 | if (typeof exports !== 'undefined') { 14 | if (typeof module !== 'undefined' && module.exports) { 15 | exports = module.exports = EXIF; 16 | } 17 | exports.EXIF = EXIF; 18 | } else { 19 | root.EXIF = EXIF; 20 | } 21 | 22 | var ExifTags = EXIF.Tags = { 23 | 24 | // version tags 25 | 0x9000 : "ExifVersion", // EXIF version 26 | 0xA000 : "FlashpixVersion", // Flashpix format version 27 | 28 | // colorspace tags 29 | 0xA001 : "ColorSpace", // Color space information tag 30 | 31 | // image configuration 32 | 0xA002 : "PixelXDimension", // Valid width of meaningful image 33 | 0xA003 : "PixelYDimension", // Valid height of meaningful image 34 | 0x9101 : "ComponentsConfiguration", // Information about channels 35 | 0x9102 : "CompressedBitsPerPixel", // Compressed bits per pixel 36 | 37 | // user information 38 | 0x927C : "MakerNote", // Any desired information written by the manufacturer 39 | 0x9286 : "UserComment", // Comments by user 40 | 41 | // related file 42 | 0xA004 : "RelatedSoundFile", // Name of related sound file 43 | 44 | // date and time 45 | 0x9003 : "DateTimeOriginal", // Date and time when the original image was generated 46 | 0x9004 : "DateTimeDigitized", // Date and time when the image was stored digitally 47 | 0x9290 : "SubsecTime", // Fractions of seconds for DateTime 48 | 0x9291 : "SubsecTimeOriginal", // Fractions of seconds for DateTimeOriginal 49 | 0x9292 : "SubsecTimeDigitized", // Fractions of seconds for DateTimeDigitized 50 | 51 | // picture-taking conditions 52 | 0x829A : "ExposureTime", // Exposure time (in seconds) 53 | 0x829D : "FNumber", // F number 54 | 0x8822 : "ExposureProgram", // Exposure program 55 | 0x8824 : "SpectralSensitivity", // Spectral sensitivity 56 | 0x8827 : "ISOSpeedRatings", // ISO speed rating 57 | 0x8828 : "OECF", // Optoelectric conversion factor 58 | 0x9201 : "ShutterSpeedValue", // Shutter speed 59 | 0x9202 : "ApertureValue", // Lens aperture 60 | 0x9203 : "BrightnessValue", // Value of brightness 61 | 0x9204 : "ExposureBias", // Exposure bias 62 | 0x9205 : "MaxApertureValue", // Smallest F number of lens 63 | 0x9206 : "SubjectDistance", // Distance to subject in meters 64 | 0x9207 : "MeteringMode", // Metering mode 65 | 0x9208 : "LightSource", // Kind of light source 66 | 0x9209 : "Flash", // Flash status 67 | 0x9214 : "SubjectArea", // Location and area of main subject 68 | 0x920A : "FocalLength", // Focal length of the lens in mm 69 | 0xA20B : "FlashEnergy", // Strobe energy in BCPS 70 | 0xA20C : "SpatialFrequencyResponse", // 71 | 0xA20E : "FocalPlaneXResolution", // Number of pixels in width direction per FocalPlaneResolutionUnit 72 | 0xA20F : "FocalPlaneYResolution", // Number of pixels in height direction per FocalPlaneResolutionUnit 73 | 0xA210 : "FocalPlaneResolutionUnit", // Unit for measuring FocalPlaneXResolution and FocalPlaneYResolution 74 | 0xA214 : "SubjectLocation", // Location of subject in image 75 | 0xA215 : "ExposureIndex", // Exposure index selected on camera 76 | 0xA217 : "SensingMethod", // Image sensor type 77 | 0xA300 : "FileSource", // Image source (3 == DSC) 78 | 0xA301 : "SceneType", // Scene type (1 == directly photographed) 79 | 0xA302 : "CFAPattern", // Color filter array geometric pattern 80 | 0xA401 : "CustomRendered", // Special processing 81 | 0xA402 : "ExposureMode", // Exposure mode 82 | 0xA403 : "WhiteBalance", // 1 = auto white balance, 2 = manual 83 | 0xA404 : "DigitalZoomRation", // Digital zoom ratio 84 | 0xA405 : "FocalLengthIn35mmFilm", // Equivalent foacl length assuming 35mm film camera (in mm) 85 | 0xA406 : "SceneCaptureType", // Type of scene 86 | 0xA407 : "GainControl", // Degree of overall image gain adjustment 87 | 0xA408 : "Contrast", // Direction of contrast processing applied by camera 88 | 0xA409 : "Saturation", // Direction of saturation processing applied by camera 89 | 0xA40A : "Sharpness", // Direction of sharpness processing applied by camera 90 | 0xA40B : "DeviceSettingDescription", // 91 | 0xA40C : "SubjectDistanceRange", // Distance to subject 92 | 93 | // other tags 94 | 0xA005 : "InteroperabilityIFDPointer", 95 | 0xA420 : "ImageUniqueID" // Identifier assigned uniquely to each image 96 | }; 97 | 98 | var TiffTags = EXIF.TiffTags = { 99 | 0x0100 : "ImageWidth", 100 | 0x0101 : "ImageHeight", 101 | 0x8769 : "ExifIFDPointer", 102 | 0x8825 : "GPSInfoIFDPointer", 103 | 0xA005 : "InteroperabilityIFDPointer", 104 | 0x0102 : "BitsPerSample", 105 | 0x0103 : "Compression", 106 | 0x0106 : "PhotometricInterpretation", 107 | 0x0112 : "Orientation", 108 | 0x0115 : "SamplesPerPixel", 109 | 0x011C : "PlanarConfiguration", 110 | 0x0212 : "YCbCrSubSampling", 111 | 0x0213 : "YCbCrPositioning", 112 | 0x011A : "XResolution", 113 | 0x011B : "YResolution", 114 | 0x0128 : "ResolutionUnit", 115 | 0x0111 : "StripOffsets", 116 | 0x0116 : "RowsPerStrip", 117 | 0x0117 : "StripByteCounts", 118 | 0x0201 : "JPEGInterchangeFormat", 119 | 0x0202 : "JPEGInterchangeFormatLength", 120 | 0x012D : "TransferFunction", 121 | 0x013E : "WhitePoint", 122 | 0x013F : "PrimaryChromaticities", 123 | 0x0211 : "YCbCrCoefficients", 124 | 0x0214 : "ReferenceBlackWhite", 125 | 0x0132 : "DateTime", 126 | 0x010E : "ImageDescription", 127 | 0x010F : "Make", 128 | 0x0110 : "Model", 129 | 0x0131 : "Software", 130 | 0x013B : "Artist", 131 | 0x8298 : "Copyright" 132 | }; 133 | 134 | var GPSTags = EXIF.GPSTags = { 135 | 0x0000 : "GPSVersionID", 136 | 0x0001 : "GPSLatitudeRef", 137 | 0x0002 : "GPSLatitude", 138 | 0x0003 : "GPSLongitudeRef", 139 | 0x0004 : "GPSLongitude", 140 | 0x0005 : "GPSAltitudeRef", 141 | 0x0006 : "GPSAltitude", 142 | 0x0007 : "GPSTimeStamp", 143 | 0x0008 : "GPSSatellites", 144 | 0x0009 : "GPSStatus", 145 | 0x000A : "GPSMeasureMode", 146 | 0x000B : "GPSDOP", 147 | 0x000C : "GPSSpeedRef", 148 | 0x000D : "GPSSpeed", 149 | 0x000E : "GPSTrackRef", 150 | 0x000F : "GPSTrack", 151 | 0x0010 : "GPSImgDirectionRef", 152 | 0x0011 : "GPSImgDirection", 153 | 0x0012 : "GPSMapDatum", 154 | 0x0013 : "GPSDestLatitudeRef", 155 | 0x0014 : "GPSDestLatitude", 156 | 0x0015 : "GPSDestLongitudeRef", 157 | 0x0016 : "GPSDestLongitude", 158 | 0x0017 : "GPSDestBearingRef", 159 | 0x0018 : "GPSDestBearing", 160 | 0x0019 : "GPSDestDistanceRef", 161 | 0x001A : "GPSDestDistance", 162 | 0x001B : "GPSProcessingMethod", 163 | 0x001C : "GPSAreaInformation", 164 | 0x001D : "GPSDateStamp", 165 | 0x001E : "GPSDifferential" 166 | }; 167 | 168 | // EXIF 2.3 Spec 169 | var IFD1Tags = EXIF.IFD1Tags = { 170 | 0x0100: "ImageWidth", 171 | 0x0101: "ImageHeight", 172 | 0x0102: "BitsPerSample", 173 | 0x0103: "Compression", 174 | 0x0106: "PhotometricInterpretation", 175 | 0x0111: "StripOffsets", 176 | 0x0112: "Orientation", 177 | 0x0115: "SamplesPerPixel", 178 | 0x0116: "RowsPerStrip", 179 | 0x0117: "StripByteCounts", 180 | 0x011A: "XResolution", 181 | 0x011B: "YResolution", 182 | 0x011C: "PlanarConfiguration", 183 | 0x0128: "ResolutionUnit", 184 | 0x0201: "JpegIFOffset", // When image format is JPEG, this value show offset to JPEG data stored.(aka "ThumbnailOffset" or "JPEGInterchangeFormat") 185 | 0x0202: "JpegIFByteCount", // When image format is JPEG, this value shows data size of JPEG image (aka "ThumbnailLength" or "JPEGInterchangeFormatLength") 186 | 0x0211: "YCbCrCoefficients", 187 | 0x0212: "YCbCrSubSampling", 188 | 0x0213: "YCbCrPositioning", 189 | 0x0214: "ReferenceBlackWhite" 190 | }; 191 | 192 | var StringValues = EXIF.StringValues = { 193 | ExposureProgram : { 194 | 0 : "Not defined", 195 | 1 : "Manual", 196 | 2 : "Normal program", 197 | 3 : "Aperture priority", 198 | 4 : "Shutter priority", 199 | 5 : "Creative program", 200 | 6 : "Action program", 201 | 7 : "Portrait mode", 202 | 8 : "Landscape mode" 203 | }, 204 | MeteringMode : { 205 | 0 : "Unknown", 206 | 1 : "Average", 207 | 2 : "CenterWeightedAverage", 208 | 3 : "Spot", 209 | 4 : "MultiSpot", 210 | 5 : "Pattern", 211 | 6 : "Partial", 212 | 255 : "Other" 213 | }, 214 | LightSource : { 215 | 0 : "Unknown", 216 | 1 : "Daylight", 217 | 2 : "Fluorescent", 218 | 3 : "Tungsten (incandescent light)", 219 | 4 : "Flash", 220 | 9 : "Fine weather", 221 | 10 : "Cloudy weather", 222 | 11 : "Shade", 223 | 12 : "Daylight fluorescent (D 5700 - 7100K)", 224 | 13 : "Day white fluorescent (N 4600 - 5400K)", 225 | 14 : "Cool white fluorescent (W 3900 - 4500K)", 226 | 15 : "White fluorescent (WW 3200 - 3700K)", 227 | 17 : "Standard light A", 228 | 18 : "Standard light B", 229 | 19 : "Standard light C", 230 | 20 : "D55", 231 | 21 : "D65", 232 | 22 : "D75", 233 | 23 : "D50", 234 | 24 : "ISO studio tungsten", 235 | 255 : "Other" 236 | }, 237 | Flash : { 238 | 0x0000 : "Flash did not fire", 239 | 0x0001 : "Flash fired", 240 | 0x0005 : "Strobe return light not detected", 241 | 0x0007 : "Strobe return light detected", 242 | 0x0009 : "Flash fired, compulsory flash mode", 243 | 0x000D : "Flash fired, compulsory flash mode, return light not detected", 244 | 0x000F : "Flash fired, compulsory flash mode, return light detected", 245 | 0x0010 : "Flash did not fire, compulsory flash mode", 246 | 0x0018 : "Flash did not fire, auto mode", 247 | 0x0019 : "Flash fired, auto mode", 248 | 0x001D : "Flash fired, auto mode, return light not detected", 249 | 0x001F : "Flash fired, auto mode, return light detected", 250 | 0x0020 : "No flash function", 251 | 0x0041 : "Flash fired, red-eye reduction mode", 252 | 0x0045 : "Flash fired, red-eye reduction mode, return light not detected", 253 | 0x0047 : "Flash fired, red-eye reduction mode, return light detected", 254 | 0x0049 : "Flash fired, compulsory flash mode, red-eye reduction mode", 255 | 0x004D : "Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected", 256 | 0x004F : "Flash fired, compulsory flash mode, red-eye reduction mode, return light detected", 257 | 0x0059 : "Flash fired, auto mode, red-eye reduction mode", 258 | 0x005D : "Flash fired, auto mode, return light not detected, red-eye reduction mode", 259 | 0x005F : "Flash fired, auto mode, return light detected, red-eye reduction mode" 260 | }, 261 | SensingMethod : { 262 | 1 : "Not defined", 263 | 2 : "One-chip color area sensor", 264 | 3 : "Two-chip color area sensor", 265 | 4 : "Three-chip color area sensor", 266 | 5 : "Color sequential area sensor", 267 | 7 : "Trilinear sensor", 268 | 8 : "Color sequential linear sensor" 269 | }, 270 | SceneCaptureType : { 271 | 0 : "Standard", 272 | 1 : "Landscape", 273 | 2 : "Portrait", 274 | 3 : "Night scene" 275 | }, 276 | SceneType : { 277 | 1 : "Directly photographed" 278 | }, 279 | CustomRendered : { 280 | 0 : "Normal process", 281 | 1 : "Custom process" 282 | }, 283 | WhiteBalance : { 284 | 0 : "Auto white balance", 285 | 1 : "Manual white balance" 286 | }, 287 | GainControl : { 288 | 0 : "None", 289 | 1 : "Low gain up", 290 | 2 : "High gain up", 291 | 3 : "Low gain down", 292 | 4 : "High gain down" 293 | }, 294 | Contrast : { 295 | 0 : "Normal", 296 | 1 : "Soft", 297 | 2 : "Hard" 298 | }, 299 | Saturation : { 300 | 0 : "Normal", 301 | 1 : "Low saturation", 302 | 2 : "High saturation" 303 | }, 304 | Sharpness : { 305 | 0 : "Normal", 306 | 1 : "Soft", 307 | 2 : "Hard" 308 | }, 309 | SubjectDistanceRange : { 310 | 0 : "Unknown", 311 | 1 : "Macro", 312 | 2 : "Close view", 313 | 3 : "Distant view" 314 | }, 315 | FileSource : { 316 | 3 : "DSC" 317 | }, 318 | 319 | Components : { 320 | 0 : "", 321 | 1 : "Y", 322 | 2 : "Cb", 323 | 3 : "Cr", 324 | 4 : "R", 325 | 5 : "G", 326 | 6 : "B" 327 | } 328 | }; 329 | 330 | function addEvent(element, event, handler) { 331 | if (element.addEventListener) { 332 | element.addEventListener(event, handler, false); 333 | } else if (element.attachEvent) { 334 | element.attachEvent("on" + event, handler); 335 | } 336 | } 337 | 338 | function imageHasData(img) { 339 | return !!(img.exifdata); 340 | } 341 | 342 | 343 | function base64ToArrayBuffer(base64, contentType) { 344 | contentType = contentType || base64.match(/^data\:([^\;]+)\;base64,/mi)[1] || ''; // e.g. 'data:image/jpeg;base64,...' => 'image/jpeg' 345 | base64 = base64.replace(/^data\:([^\;]+)\;base64,/gmi, ''); 346 | var binary = atob(base64); 347 | var len = binary.length; 348 | var buffer = new ArrayBuffer(len); 349 | var view = new Uint8Array(buffer); 350 | for (var i = 0; i < len; i++) { 351 | view[i] = binary.charCodeAt(i); 352 | } 353 | return buffer; 354 | } 355 | 356 | function objectURLToBlob(url, callback) { 357 | var http = new XMLHttpRequest(); 358 | http.open("GET", url, true); 359 | http.responseType = "blob"; 360 | http.onload = function(e) { 361 | if (this.status == 200 || this.status === 0) { 362 | callback(this.response); 363 | } 364 | }; 365 | http.send(); 366 | } 367 | 368 | function getImageData(img, callback) { 369 | function handleBinaryFile(binFile) { 370 | var data = findEXIFinJPEG(binFile); 371 | img.exifdata = data || {}; 372 | var iptcdata = findIPTCinJPEG(binFile); 373 | img.iptcdata = iptcdata || {}; 374 | if (EXIF.isXmpEnabled) { 375 | var xmpdata= findXMPinJPEG(binFile); 376 | img.xmpdata = xmpdata || {}; 377 | } 378 | if (callback) { 379 | callback.call(img); 380 | } 381 | } 382 | 383 | if (img.src) { 384 | if (/^data\:/i.test(img.src)) { // Data URI 385 | var arrayBuffer = base64ToArrayBuffer(img.src); 386 | handleBinaryFile(arrayBuffer); 387 | 388 | } else if (/^blob\:/i.test(img.src)) { // Object URL 389 | var fileReader = new FileReader(); 390 | fileReader.onload = function(e) { 391 | handleBinaryFile(e.target.result); 392 | }; 393 | objectURLToBlob(img.src, function (blob) { 394 | fileReader.readAsArrayBuffer(blob); 395 | }); 396 | } else { 397 | var http = new XMLHttpRequest(); 398 | http.onload = function() { 399 | if (this.status == 200 || this.status === 0) { 400 | handleBinaryFile(http.response); 401 | } else { 402 | throw "Could not load image"; 403 | } 404 | http = null; 405 | }; 406 | http.open("GET", img.src, true); 407 | http.responseType = "arraybuffer"; 408 | http.send(null); 409 | } 410 | } else if (self.FileReader && (img instanceof self.Blob || img instanceof self.File)) { 411 | var fileReader = new FileReader(); 412 | fileReader.onload = function(e) { 413 | if (debug) console.log("Got file of length " + e.target.result.byteLength); 414 | handleBinaryFile(e.target.result); 415 | }; 416 | 417 | fileReader.readAsArrayBuffer(img); 418 | } 419 | } 420 | 421 | function findEXIFinJPEG(file) { 422 | var dataView = new DataView(file); 423 | 424 | if (debug) console.log("Got file of length " + file.byteLength); 425 | if ((dataView.getUint8(0) != 0xFF) || (dataView.getUint8(1) != 0xD8)) { 426 | if (debug) console.log("Not a valid JPEG"); 427 | return false; // not a valid jpeg 428 | } 429 | 430 | var offset = 2, 431 | length = file.byteLength, 432 | marker; 433 | 434 | while (offset < length) { 435 | if (dataView.getUint8(offset) != 0xFF) { 436 | if (debug) console.log("Not a valid marker at offset " + offset + ", found: " + dataView.getUint8(offset)); 437 | return false; // not a valid marker, something is wrong 438 | } 439 | 440 | marker = dataView.getUint8(offset + 1); 441 | if (debug) console.log(marker); 442 | 443 | // we could implement handling for other markers here, 444 | // but we're only looking for 0xFFE1 for EXIF data 445 | 446 | if (marker == 225) { 447 | if (debug) console.log("Found 0xFFE1 marker"); 448 | 449 | return readEXIFData(dataView, offset + 4, dataView.getUint16(offset + 2) - 2); 450 | 451 | // offset += 2 + file.getShortAt(offset+2, true); 452 | 453 | } else { 454 | offset += 2 + dataView.getUint16(offset+2); 455 | } 456 | 457 | } 458 | 459 | } 460 | 461 | function findIPTCinJPEG(file) { 462 | var dataView = new DataView(file); 463 | 464 | if (debug) console.log("Got file of length " + file.byteLength); 465 | if ((dataView.getUint8(0) != 0xFF) || (dataView.getUint8(1) != 0xD8)) { 466 | if (debug) console.log("Not a valid JPEG"); 467 | return false; // not a valid jpeg 468 | } 469 | 470 | var offset = 2, 471 | length = file.byteLength; 472 | 473 | 474 | var isFieldSegmentStart = function(dataView, offset){ 475 | return ( 476 | dataView.getUint8(offset) === 0x38 && 477 | dataView.getUint8(offset+1) === 0x42 && 478 | dataView.getUint8(offset+2) === 0x49 && 479 | dataView.getUint8(offset+3) === 0x4D && 480 | dataView.getUint8(offset+4) === 0x04 && 481 | dataView.getUint8(offset+5) === 0x04 482 | ); 483 | }; 484 | 485 | while (offset < length) { 486 | 487 | if ( isFieldSegmentStart(dataView, offset )){ 488 | 489 | // Get the length of the name header (which is padded to an even number of bytes) 490 | var nameHeaderLength = dataView.getUint8(offset+7); 491 | if(nameHeaderLength % 2 !== 0) nameHeaderLength += 1; 492 | // Check for pre photoshop 6 format 493 | if(nameHeaderLength === 0) { 494 | // Always 4 495 | nameHeaderLength = 4; 496 | } 497 | 498 | var startOffset = offset + 8 + nameHeaderLength; 499 | var sectionLength = dataView.getUint16(offset + 6 + nameHeaderLength); 500 | 501 | return readIPTCData(file, startOffset, sectionLength); 502 | 503 | break; 504 | 505 | } 506 | 507 | 508 | // Not the marker, continue searching 509 | offset++; 510 | 511 | } 512 | 513 | } 514 | var IptcFieldMap = { 515 | 0x78 : 'caption', 516 | 0x6E : 'credit', 517 | 0x19 : 'keywords', 518 | 0x37 : 'dateCreated', 519 | 0x50 : 'byline', 520 | 0x55 : 'bylineTitle', 521 | 0x7A : 'captionWriter', 522 | 0x69 : 'headline', 523 | 0x74 : 'copyright', 524 | 0x0F : 'category' 525 | }; 526 | function readIPTCData(file, startOffset, sectionLength){ 527 | var dataView = new DataView(file); 528 | var data = {}; 529 | var fieldValue, fieldName, dataSize, segmentType, segmentSize; 530 | var segmentStartPos = startOffset; 531 | while(segmentStartPos < startOffset+sectionLength) { 532 | if(dataView.getUint8(segmentStartPos) === 0x1C && dataView.getUint8(segmentStartPos+1) === 0x02){ 533 | segmentType = dataView.getUint8(segmentStartPos+2); 534 | if(segmentType in IptcFieldMap) { 535 | dataSize = dataView.getInt16(segmentStartPos+3); 536 | segmentSize = dataSize + 5; 537 | fieldName = IptcFieldMap[segmentType]; 538 | fieldValue = getStringFromDB(dataView, segmentStartPos+5, dataSize); 539 | // Check if we already stored a value with this name 540 | if(data.hasOwnProperty(fieldName)) { 541 | // Value already stored with this name, create multivalue field 542 | if(data[fieldName] instanceof Array) { 543 | data[fieldName].push(fieldValue); 544 | } 545 | else { 546 | data[fieldName] = [data[fieldName], fieldValue]; 547 | } 548 | } 549 | else { 550 | data[fieldName] = fieldValue; 551 | } 552 | } 553 | 554 | } 555 | segmentStartPos++; 556 | } 557 | return data; 558 | } 559 | 560 | 561 | 562 | function readTags(file, tiffStart, dirStart, strings, bigEnd) { 563 | var entries = file.getUint16(dirStart, !bigEnd), 564 | tags = {}, 565 | entryOffset, tag, 566 | i; 567 | 568 | for (i=0;i 4 ? valueOffset : (entryOffset + 8); 593 | vals = []; 594 | for (n=0;n 4 ? valueOffset : (entryOffset + 8); 602 | return getStringFromDB(file, offset, numValues-1); 603 | 604 | case 3: // short, 16 bit int 605 | if (numValues == 1) { 606 | return file.getUint16(entryOffset + 8, !bigEnd); 607 | } else { 608 | offset = numValues > 2 ? valueOffset : (entryOffset + 8); 609 | vals = []; 610 | for (n=0;n dataView.byteLength) { // this should not happen 695 | // console.log('******** IFD1Offset is outside the bounds of the DataView ********'); 696 | return {}; 697 | } 698 | // console.log('******* thumbnail IFD offset (IFD1) is: %s', IFD1OffsetPointer); 699 | 700 | var thumbTags = readTags(dataView, tiffStart, tiffStart + IFD1OffsetPointer, IFD1Tags, bigEnd) 701 | 702 | // EXIF 2.3 specification for JPEG format thumbnail 703 | 704 | // If the value of Compression(0x0103) Tag in IFD1 is '6', thumbnail image format is JPEG. 705 | // Most of Exif image uses JPEG format for thumbnail. In that case, you can get offset of thumbnail 706 | // by JpegIFOffset(0x0201) Tag in IFD1, size of thumbnail by JpegIFByteCount(0x0202) Tag. 707 | // Data format is ordinary JPEG format, starts from 0xFFD8 and ends by 0xFFD9. It seems that 708 | // JPEG format and 160x120pixels of size are recommended thumbnail format for Exif2.1 or later. 709 | 710 | if (thumbTags['Compression']) { 711 | // console.log('Thumbnail image found!'); 712 | 713 | switch (thumbTags['Compression']) { 714 | case 6: 715 | // console.log('Thumbnail image format is JPEG'); 716 | if (thumbTags.JpegIFOffset && thumbTags.JpegIFByteCount) { 717 | // extract the thumbnail 718 | var tOffset = tiffStart + thumbTags.JpegIFOffset; 719 | var tLength = thumbTags.JpegIFByteCount; 720 | thumbTags['blob'] = new Blob([new Uint8Array(dataView.buffer, tOffset, tLength)], { 721 | type: 'image/jpeg' 722 | }); 723 | } 724 | break; 725 | 726 | case 1: 727 | console.log("Thumbnail image format is TIFF, which is not implemented."); 728 | break; 729 | default: 730 | console.log("Unknown thumbnail image format '%s'", thumbTags['Compression']); 731 | } 732 | } 733 | else if (thumbTags['PhotometricInterpretation'] == 2) { 734 | console.log("Thumbnail image format is RGB, which is not implemented."); 735 | } 736 | return thumbTags; 737 | } 738 | 739 | function getStringFromDB(buffer, start, length) { 740 | var outstr = ""; 741 | for (var n = start; n < start+length; n++) { 742 | outstr += String.fromCharCode(buffer.getUint8(n)); 743 | } 744 | return outstr; 745 | } 746 | 747 | function readEXIFData(file, start) { 748 | if (getStringFromDB(file, start, 4) != "Exif") { 749 | if (debug) console.log("Not valid EXIF data! " + getStringFromDB(file, start, 4)); 750 | return false; 751 | } 752 | 753 | var bigEnd, 754 | tags, tag, 755 | exifData, gpsData, 756 | tiffOffset = start + 6; 757 | 758 | // test for TIFF validity and endianness 759 | if (file.getUint16(tiffOffset) == 0x4949) { 760 | bigEnd = false; 761 | } else if (file.getUint16(tiffOffset) == 0x4D4D) { 762 | bigEnd = true; 763 | } else { 764 | if (debug) console.log("Not valid TIFF data! (no 0x4949 or 0x4D4D)"); 765 | return false; 766 | } 767 | 768 | if (file.getUint16(tiffOffset+2, !bigEnd) != 0x002A) { 769 | if (debug) console.log("Not valid TIFF data! (no 0x002A)"); 770 | return false; 771 | } 772 | 773 | var firstIFDOffset = file.getUint32(tiffOffset+4, !bigEnd); 774 | 775 | if (firstIFDOffset < 0x00000008) { 776 | if (debug) console.log("Not valid TIFF data! (First offset less than 8)", file.getUint32(tiffOffset+4, !bigEnd)); 777 | return false; 778 | } 779 | 780 | tags = readTags(file, tiffOffset, tiffOffset + firstIFDOffset, TiffTags, bigEnd); 781 | 782 | if (tags.ExifIFDPointer) { 783 | exifData = readTags(file, tiffOffset, tiffOffset + tags.ExifIFDPointer, ExifTags, bigEnd); 784 | for (tag in exifData) { 785 | switch (tag) { 786 | case "LightSource" : 787 | case "Flash" : 788 | case "MeteringMode" : 789 | case "ExposureProgram" : 790 | case "SensingMethod" : 791 | case "SceneCaptureType" : 792 | case "SceneType" : 793 | case "CustomRendered" : 794 | case "WhiteBalance" : 795 | case "GainControl" : 796 | case "Contrast" : 797 | case "Saturation" : 798 | case "Sharpness" : 799 | case "SubjectDistanceRange" : 800 | case "FileSource" : 801 | exifData[tag] = StringValues[tag][exifData[tag]]; 802 | break; 803 | 804 | case "ExifVersion" : 805 | case "FlashpixVersion" : 806 | exifData[tag] = String.fromCharCode(exifData[tag][0], exifData[tag][1], exifData[tag][2], exifData[tag][3]); 807 | break; 808 | 809 | case "ComponentsConfiguration" : 810 | exifData[tag] = 811 | StringValues.Components[exifData[tag][0]] + 812 | StringValues.Components[exifData[tag][1]] + 813 | StringValues.Components[exifData[tag][2]] + 814 | StringValues.Components[exifData[tag][3]]; 815 | break; 816 | } 817 | tags[tag] = exifData[tag]; 818 | } 819 | } 820 | 821 | if (tags.GPSInfoIFDPointer) { 822 | gpsData = readTags(file, tiffOffset, tiffOffset + tags.GPSInfoIFDPointer, GPSTags, bigEnd); 823 | for (tag in gpsData) { 824 | switch (tag) { 825 | case "GPSVersionID" : 826 | gpsData[tag] = gpsData[tag][0] + 827 | "." + gpsData[tag][1] + 828 | "." + gpsData[tag][2] + 829 | "." + gpsData[tag][3]; 830 | break; 831 | } 832 | tags[tag] = gpsData[tag]; 833 | } 834 | } 835 | 836 | // extract thumbnail 837 | tags['thumbnail'] = readThumbnailImage(file, tiffOffset, firstIFDOffset, bigEnd); 838 | 839 | return tags; 840 | } 841 | 842 | function findXMPinJPEG(file) { 843 | 844 | if (!('DOMParser' in self)) { 845 | // console.warn('XML parsing not supported without DOMParser'); 846 | return; 847 | } 848 | var dataView = new DataView(file); 849 | 850 | if (debug) console.log("Got file of length " + file.byteLength); 851 | if ((dataView.getUint8(0) != 0xFF) || (dataView.getUint8(1) != 0xD8)) { 852 | if (debug) console.log("Not a valid JPEG"); 853 | return false; // not a valid jpeg 854 | } 855 | 856 | var offset = 2, 857 | length = file.byteLength, 858 | dom = new DOMParser(); 859 | 860 | while (offset < (length-4)) { 861 | if (getStringFromDB(dataView, offset, 4) == "http") { 862 | var startOffset = offset - 1; 863 | var sectionLength = dataView.getUint16(offset - 2) - 1; 864 | var xmpString = getStringFromDB(dataView, startOffset, sectionLength) 865 | var xmpEndIndex = xmpString.indexOf('xmpmeta>') + 8; 866 | xmpString = xmpString.substring( xmpString.indexOf( ' 0) { 898 | json['@attributes'] = {}; 899 | for (var j = 0; j < xml.attributes.length; j++) { 900 | var attribute = xml.attributes.item(j); 901 | json['@attributes'][attribute.nodeName] = attribute.nodeValue; 902 | } 903 | } 904 | } else if (xml.nodeType == 3) { // text node 905 | return xml.nodeValue; 906 | } 907 | 908 | // deal with children 909 | if (xml.hasChildNodes()) { 910 | for(var i = 0; i < xml.childNodes.length; i++) { 911 | var child = xml.childNodes.item(i); 912 | var nodeName = child.nodeName; 913 | if (json[nodeName] == null) { 914 | json[nodeName] = xml2json(child); 915 | } else { 916 | if (json[nodeName].push == null) { 917 | var old = json[nodeName]; 918 | json[nodeName] = []; 919 | json[nodeName].push(old); 920 | } 921 | json[nodeName].push(xml2json(child)); 922 | } 923 | } 924 | } 925 | 926 | return json; 927 | } 928 | 929 | function xml2Object(xml) { 930 | try { 931 | var obj = {}; 932 | if (xml.children.length > 0) { 933 | for (var i = 0; i < xml.children.length; i++) { 934 | var item = xml.children.item(i); 935 | var attributes = item.attributes; 936 | for(var idx in attributes) { 937 | var itemAtt = attributes[idx]; 938 | var dataKey = itemAtt.nodeName; 939 | var dataValue = itemAtt.nodeValue; 940 | 941 | if(dataKey !== undefined) { 942 | obj[dataKey] = dataValue; 943 | } 944 | } 945 | var nodeName = item.nodeName; 946 | 947 | if (typeof (obj[nodeName]) == "undefined") { 948 | obj[nodeName] = xml2json(item); 949 | } else { 950 | if (typeof (obj[nodeName].push) == "undefined") { 951 | var old = obj[nodeName]; 952 | 953 | obj[nodeName] = []; 954 | obj[nodeName].push(old); 955 | } 956 | obj[nodeName].push(xml2json(item)); 957 | } 958 | } 959 | } else { 960 | obj = xml.textContent; 961 | } 962 | return obj; 963 | } catch (e) { 964 | console.log(e.message); 965 | } 966 | } 967 | 968 | EXIF.enableXmp = function() { 969 | EXIF.isXmpEnabled = true; 970 | } 971 | 972 | EXIF.disableXmp = function() { 973 | EXIF.isXmpEnabled = false; 974 | } 975 | 976 | EXIF.getData = function(img, callback) { 977 | if (((self.Image && img instanceof self.Image) 978 | || (self.HTMLImageElement && img instanceof self.HTMLImageElement)) 979 | && !img.complete) 980 | return false; 981 | 982 | if (!imageHasData(img)) { 983 | getImageData(img, callback); 984 | } else { 985 | if (callback) { 986 | callback.call(img); 987 | } 988 | } 989 | return true; 990 | } 991 | 992 | EXIF.getTag = function(img, tag) { 993 | if (!imageHasData(img)) return; 994 | return img.exifdata[tag]; 995 | } 996 | 997 | EXIF.getIptcTag = function(img, tag) { 998 | if (!imageHasData(img)) return; 999 | return img.iptcdata[tag]; 1000 | } 1001 | 1002 | EXIF.getAllTags = function(img) { 1003 | if (!imageHasData(img)) return {}; 1004 | var a, 1005 | data = img.exifdata, 1006 | tags = {}; 1007 | for (a in data) { 1008 | if (data.hasOwnProperty(a)) { 1009 | tags[a] = data[a]; 1010 | } 1011 | } 1012 | return tags; 1013 | } 1014 | 1015 | EXIF.getAllIptcTags = function(img) { 1016 | if (!imageHasData(img)) return {}; 1017 | var a, 1018 | data = img.iptcdata, 1019 | tags = {}; 1020 | for (a in data) { 1021 | if (data.hasOwnProperty(a)) { 1022 | tags[a] = data[a]; 1023 | } 1024 | } 1025 | return tags; 1026 | } 1027 | 1028 | EXIF.pretty = function(img) { 1029 | if (!imageHasData(img)) return ""; 1030 | var a, 1031 | data = img.exifdata, 1032 | strPretty = ""; 1033 | for (a in data) { 1034 | if (data.hasOwnProperty(a)) { 1035 | if (typeof data[a] == "object") { 1036 | if (data[a] instanceof Number) { 1037 | strPretty += a + " : " + data[a] + " [" + data[a].numerator + "/" + data[a].denominator + "]\r\n"; 1038 | } else { 1039 | strPretty += a + " : [" + data[a].length + " values]\r\n"; 1040 | } 1041 | } else { 1042 | strPretty += a + " : " + data[a] + "\r\n"; 1043 | } 1044 | } 1045 | } 1046 | return strPretty; 1047 | } 1048 | 1049 | EXIF.readFromBinaryFile = function(file) { 1050 | return findEXIFinJPEG(file); 1051 | } 1052 | 1053 | if (typeof define === 'function' && define.amd) { 1054 | define('exif-js', [], function() { 1055 | return EXIF; 1056 | }); 1057 | } 1058 | }.call(this)); 1059 | 1060 | --------------------------------------------------------------------------------