From 7c70ce1b5f501ebfaf858e1656f7cd714b601328 Mon Sep 17 00:00:00 2001 From: Karaviro <131587924+Karaviro@users.noreply.github.com> Date: Fri, 16 Jan 2026 04:53:32 +0100 Subject: [PATCH 01/10] Delete manifest.json --- manifest.json | 50 -------------------------------------------------- 1 file changed, 50 deletions(-) delete mode 100644 manifest.json diff --git a/manifest.json b/manifest.json deleted file mode 100644 index 26029c8..0000000 --- a/manifest.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "description": "Pulls images out of the current tab", - "manifest_version": 3, - "homepage_url": "https://github.com/Skeletonxf/image-extract", - "name": "Image extract", - "version": "2.3", - - "permissions": ["menus", "activeTab", "storage", "scripting"], - - "background": { - "page": "/background-page.html" - }, - - "browser_specific_settings": { - "gecko": { - "id": "{6381b0dc-8bef-47d0-8c31-6fffbdd8ec7f}", - "strict_min_version": "129.0" - } - }, - - "action": { - "default_icon": "icons/arrow.svg", - "default_title": "Image extract" - }, - - "options_ui": { - "page": "/settings/settings.html" - }, - - "commands": { - "extract-shortcut-1": { - "suggested_key": { - "default": "Ctrl+E" - }, - "description": "Run image extract on current tab from keyboard shortcut (1)" - }, - "extract-shortcut-2": { - "suggested_key": { - "default": "Alt+Shift+E" - }, - "description": "Run image extract on current tab from keyboard shortcut (2)" - }, - "extract-shortcut-3": { - "suggested_key": { - "default": "Alt+Shift+I" - }, - "description": "Run image extract on current tab from keyboard shortcut (3)" - } - } -} From 37abc1f1b0c55b3bfd05ab039ec97915242147da Mon Sep 17 00:00:00 2001 From: Karaviro <131587924+Karaviro@users.noreply.github.com> Date: Fri, 16 Jan 2026 04:53:50 +0100 Subject: [PATCH 02/10] Delete background-page.html --- background-page.html | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 background-page.html diff --git a/background-page.html b/background-page.html deleted file mode 100644 index 2333b8a..0000000 --- a/background-page.html +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - From b07fa167678fb56154d4154f571fa7634a2563a7 Mon Sep 17 00:00:00 2001 From: Karaviro <131587924+Karaviro@users.noreply.github.com> Date: Fri, 16 Jan 2026 04:54:13 +0100 Subject: [PATCH 03/10] Delete push.sh --- push.sh | 2 -- 1 file changed, 2 deletions(-) delete mode 100755 push.sh diff --git a/push.sh b/push.sh deleted file mode 100755 index 5ed3bb8..0000000 --- a/push.sh +++ /dev/null @@ -1,2 +0,0 @@ -# See https://extensionworkshop.com/documentation/develop/developing-extensions-for-firefox-for-android/ -web-ext run -t firefox-android --adb-device --firefox-apk From 9d24a090c1c57dc012c5253680bbc5590b254269 Mon Sep 17 00:00:00 2001 From: Karaviro <131587924+Karaviro@users.noreply.github.com> Date: Fri, 16 Jan 2026 04:55:05 +0100 Subject: [PATCH 04/10] Delete release.sh --- release.sh | 3 --- 1 file changed, 3 deletions(-) delete mode 100755 release.sh diff --git a/release.sh b/release.sh deleted file mode 100755 index eddc0be..0000000 --- a/release.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -rm addon.zip -zip -r addon.zip . -x screenshots/\* test-webpages/ push.sh release.sh .git/\* .gitignore From 37b5504f8ef0fa81500c5363d22e59602dca9f3b Mon Sep 17 00:00:00 2001 From: Karaviro <131587924+Karaviro@users.noreply.github.com> Date: Fri, 16 Jan 2026 04:55:28 +0100 Subject: [PATCH 05/10] Delete src directory --- src/content-script.js | 402 ------------------------------------------ src/content.css | 83 --------- src/context-menus.js | 44 ----- src/image-extract.js | 25 --- src/logger.js | 12 -- src/main.js | 105 ----------- src/settings.js | 51 ------ src/shortcuts.js | 36 ---- 8 files changed, 758 deletions(-) delete mode 100644 src/content-script.js delete mode 100644 src/content.css delete mode 100644 src/context-menus.js delete mode 100644 src/image-extract.js delete mode 100644 src/logger.js delete mode 100644 src/main.js delete mode 100644 src/settings.js delete mode 100644 src/shortcuts.js diff --git a/src/content-script.js b/src/content-script.js deleted file mode 100644 index c2fa994..0000000 --- a/src/content-script.js +++ /dev/null @@ -1,402 +0,0 @@ -// Wrap everything in a self executing function without a name to avoid -// conflicts when this script is injected into a page additional times -(function() { - -const idSuffix = "ImageExtract" -const settingKeys = [ - 'centerImages', - 'realSizeImages', - 'showBackgroundImages', - 'confirmBeforeRunningImageExtract' -] - -let idToSettingKeys = {} -for (const key of settingKeys) { - idToSettingKeys[key + idSuffix] = key -} - -/** - * Sets a list of attributes onto an element. - * @param element element to set attributes on - * @param attributes list of objects where name field - * is attribute name and value field is value of attribute to set. - */ -function setAttributes(element, attributes) { - attributes.forEach((attribute) => { - element.setAttribute(attribute.name, attribute.value) - }) -} - -/** - * Builds the Image Extract UI into the page to control - * the display of the extracted images. - * Returns an object of UI elements to be given function. - */ -function buildUI() { - // creates a toggle and label wrapped in a container for - // injecting a basic UI into the page - function makeToggle(name, checked, settingKey) { - // make div to go around container - let container = document.createElement("div") - container.classList.add("imageExtractUI") - // create an id from the setting key - let id = settingKey + idSuffix - // make the checkbox - let checkbox = document.createElement("input") - checkbox.classList.add("imageExtractCheckbox") - setAttributes(checkbox, [ - { - name: 'type', - value: 'checkbox' - }, - { - name: 'id', - value: id - } - ]) - if (checked) { - checkbox.setAttribute("checked", "true") - } - // make label - let label = document.createElement("label") - label.classList.add("imageExtractLabel") - setAttributes(label, [ - { - name: 'for', - value: id - } - ]) - label.appendChild(document.createTextNode(name)) - // attatch the checkbox and label to the container - container.appendChild(checkbox) - container.appendChild(label) - return { - container: container, - checkbox: checkbox - } - } - - // create a checkbox to toggle centering items - let center = makeToggle('Center', true, 'centerImages') - - // create a checkbox to toggle image sizes - let size = makeToggle('Real size', false, 'realSizeImages') - - // create a checkbox to hide background images - let background = makeToggle('Show background images', false, 'showBackgroundImages') - - // create container for UI elements - let container = document.createElement("div") - setAttributes(container, [ - { - name: 'id', - value: 'imageExtractUIContainer' - } - ]) - - // add UI to page - document.body.appendChild(container) - let firstRow = document.createElement("div") - setAttributes(firstRow, [ - { - name: 'id', - value: 'imageExtractUIRow1' - } - ]) - let secondRow = document.createElement("div") - setAttributes(secondRow, [ - { - name: 'id', - value: 'imageExtractUIRow2' - } - ]) - container.appendChild(firstRow) - - firstRow.appendChild(center.container) - firstRow.appendChild(size.container) - firstRow.appendChild(background.container) - - let confirm = makeToggle('Confirm before running', false, 'confirmBeforeRunningImageExtract') - let info = document.createElement("p") - info.textContent = "If set to confirm, Image Extract will display a confirmation dialog before running, allowing you to cancel if you didn't mean to. This can help prevent you from accidentally losing your tabs, but it adds an extra step." - container.appendChild(info) - container.appendChild(secondRow) - secondRow.appendChild(confirm.container) - - // return checkboxes for applying logic on click to - return { - center: center, - size: size, - background: background, - confirm: confirm - } -} - -/** - * Returns an array of image data objects of all images - * found on the page. - */ -function extractImages() { - // get all elements in the body - let elements = document.body.getElementsByTagName("*") - - // store copies of the data on the images in the body - let images = [] - - // avoid duplicates of the same background image - let urls = new Set() - - Array.from(elements).forEach((element) => { - if (element instanceof HTMLImageElement) { - // detect image elements on the page - let imageData = { - url: element.src, - width: element.offsetWidth, - height: element.offsetHeight, - type: 'element' - } - if (!urls.has(imageData.url)) { - images.push(imageData) - urls.add(imageData.url) - } - } - - // TODO Support data URIs - // https://css-tricks.com/data-uris/ - - let style = window.getComputedStyle(element) - for (let rule of ['backgroundImage', 'background']) { - if (style[rule]) { - // match 'url' then '(' then as few characters as possible then ')' - let re = /url\(.*?\)/ - let matches = style[rule].match(re) - if (matches) { - matches.forEach((match) => { - // now we know exactly how the match is formatted we can pull - // the url out of it - let url = match.substring(5, match.length - 2) - let imageData = { - url: url, - width: element.offsetWidth, - height: element.offsetHeight, - type: 'background' - } - if (!urls.has(imageData.url)) { - images.push(imageData) - urls.add(imageData.url) - } - }) - } - } - } - }) - - return images -} - -function run(imageDatas /* [ImageData] */, uiSettings, onUpdateSettings) { - // flag to avoid running twice - window.hasRun = true - - // delete everything under the body - while (document.body.firstChild) { - document.body.firstChild.remove(); - } - - // Add the UI to the page - let ui = buildUI() - - // fill the body with the copied images - imageDatas.forEach((imageData) => { - let image = document.createElement('img') - setAttributes(image, [ - { - name: 'src', - value: imageData.url - }, - { - name: 'width', - value: imageData.width - }, - { - name: 'height', - value: imageData.height - }, - ]) - document.body.appendChild(image) - image.sizeType = 'webpage' - image.dataType = imageData.type - imageData.image = image - }) - - // add message to user - document.title = 'Refresh page to return - ' + document.title - - // Make UI elements perform changes to page - - function toggleCenter() { - // toggle the center style of each image - imageDatas.forEach((imageData) => { - imageData.image.classList.toggle('imageExtractCenterStyle') - }) - let enabled = ui.center.checkbox.checked - let id = idToSettingKeys[ui.center.checkbox.id] - let update = {} - update[id] = enabled - onUpdateSettings(update) - } - ui.center.checkbox.addEventListener('change', toggleCenter) - - function toggleSizeForImage(image, backoff /* number: remaining tries */) { - if ( - image.sizeType === 'naturalSize' && - (image.width === 0 || image.height === 0) - ) { - if (!(image.naturalWidth === 0 || image.naturalHeight === 0)) { - image.width = image.naturalWidth - image.height = image.naturalHeight - } else { - if (backoff >= 0) { - // wait a little longer in case we're checking natural size - // too early - (async() => { - await new Promise(resolve => setTimeout(resolve, 300)); - toggleSizeForImage(image, backoff - 1) - })() - } - } - } - } - function toggleSize(backoff /* boolean */) { - // flip each image between its natural size and - // the size it had on the webpage - imageDatas.forEach((imageData) => { - let image = imageData.image - if (image.sizeType === 'webpage') { - image.sizeType = 'naturalSize' - // set image size to natural size of its url - image.width = image.naturalWidth - image.height = image.naturalHeight - } else { - image.sizeType = 'webpage' - image.width = imageData.width - image.height = imageData.height - } - if ( - backoff && - image.sizeType === 'naturalSize' && - (image.naturalWidth === 0 || image.naturalHeight === 0) - ) { - // natural size may sometimes be 0 due to checking too early - (async() => { - await new Promise(resolve => setTimeout(resolve, 100)); - toggleSizeForImage(image, 5) - })() - } - }) - let enabled = ui.size.checkbox.checked - let id = idToSettingKeys[ui.size.checkbox.id] - let update = {} - update[id] = enabled - onUpdateSettings(update) - } - function toggleSizeWithBackoff() { - toggleSize(true) - } - function toggleSizeWithNoBackoff() { - toggleSize(false) - } - - ui.size.checkbox.addEventListener('change', toggleSizeWithNoBackoff) - - function toggleShowBackground() { - // flip the .hideImage style - imageDatas.forEach((imageData) => { - let image = imageData.image - if (image.dataType === 'background') { - image.classList.toggle("hideImage") - } - }) - let enabled = ui.background.checkbox.checked - let id = idToSettingKeys[ui.background.checkbox.id] - let update = {} - update[id] = enabled - onUpdateSettings(update) - } - ui.background.checkbox.addEventListener('change', toggleShowBackground) - - function toggleConfirm() { - let enabled = ui.confirm.checkbox.checked - let id = idToSettingKeys[ui.confirm.checkbox.id] - let update = {} - update[id] = enabled - onUpdateSettings(update) - } - ui.confirm.checkbox.addEventListener('change', toggleConfirm) - - if (uiSettings.centerImages) { - toggleCenter() - ui.center.checkbox.setAttribute('checked', 'true') - } else { - ui.center.checkbox.removeAttribute('checked') - } - if (uiSettings.realSizeImages) { - toggleSizeWithBackoff() - ui.size.checkbox.setAttribute('checked', 'true') - } else { - ui.size.checkbox.removeAttribute('checked') - } - if (uiSettings.showBackgroundImages) { - ui.background.checkbox.setAttribute('checked', 'true') - } else { - toggleShowBackground() - ui.background.checkbox.removeAttribute('checked') - } - if (uiSettings.confirmBeforeRunningImageExtract) { - ui.confirm.checkbox.setAttribute('checked', 'true') - } else { - ui.confirm.checkbox.removeAttribute('checked') - } -} - -function check() { - if (window.hasRun) { - // don't run twice - return - } - - let fetchingSettings = browser.runtime.sendMessage({getAllUISettings: true}) - - // get all image data from the page - let imageDatas = extractImages() - - let onUpdateSettings = async (update) => { - let result = browser.runtime.sendMessage( - { - updateUISettings: true, - update: update, - } - ) - } - - fetchingSettings.then((response) => { - if (response.uiSettings) { - if (response.confirmBeforeRunningImageExtract) { - if (window.confirm(`Run Image Extract and replace this tab's content with its ${imageDatas.length} images?`)) { - run(imageDatas, response, onUpdateSettings) - } - } else { - run(imageDatas, response, onUpdateSettings) - } - } else { - console.error('No UI settings returned in response', response) - } - }) - .catch((error) => { - console.error('Error getting UI settings', error) - }) -} - -check() - -})() diff --git a/src/content.css b/src/content.css deleted file mode 100644 index 00849dc..0000000 --- a/src/content.css +++ /dev/null @@ -1,83 +0,0 @@ -body { - margin: 0 !important; - padding: 0 !important; -} - -/* centers images in the middle of the page */ -.imageExtractCenterStyle { - margin-left: auto !important; - margin-right: auto !important; - display: block !important; -} - -/* full page navigation */ -#imageExtractUIContainer { - width: 100% !important; - background-color: lightyellow !important; - margin: 0 !important; - padding: 0 !important; - display: flex !important; - flex-direction: column !important; - justify-content: center !important; - flex-wrap: wrap !important; - position: relative !important; -} - -#imageExtractUIRow1, #imageExtractUIRow2 { - width: 100% !important; - background-color: lightyellow !important; - margin: 0 !important; - padding: 0 !important; - display: flex !important; - flex-direction: row !important; - justify-content: center !important; - flex-wrap: wrap !important; - position: relative !important; -} - -p { - font-size: small !important; - font-style: normal !important; - color: black !important; - text-align: center !important; - margin: 4px !important; - padding: 0 !important; -} - -/* each toggle + label container */ -.imageExtractUI { - margin: 0 !important; - padding: 0 !important; - display: flex !important; - flex-direction: row !important; - position: relative !important; - padding-left: 5px -} - -.imageExtractUI:nth-child(odd) { - background-color: #0000000f !important; -} - -/* align under the toggle button */ -.imageExtractLabel { - text-align: center !important; - font-family: sans-serif !important; - font-size: 14px !important; - color: black !important; - margin: auto !important; - padding: 10px !important; - padding-left: 2px !important; - position: relative !important; - padding-right: 5px -} - -.imageExtractCheckbox { - text-align: center !important; - margin: auto !important; - padding: 0 !important; - position: relative !important; -} - -.hideImage { - display: none !important; -} diff --git a/src/context-menus.js b/src/context-menus.js deleted file mode 100644 index f8db589..0000000 --- a/src/context-menus.js +++ /dev/null @@ -1,44 +0,0 @@ -const contextMenuId = 'image-extract-menu' - -export default class ContextMenus { - registerContextMenu(listener /* (Tab) -> () */) { - // will be undefined on android - if (browser.menus) { - this.#registerContextMenu(contextMenuId, listener) - } - } - - addContextMenu(listener /* (Tab) -> () */) { - // will be undefined on android - if (browser.menus) { - this.#addContextMenu(contextMenuId, 'Extract Images', listener) - } - } - - removeContextMenu(listener /* (Tab) -> () */) { - // will be undefined on android - if (browser.menus) { - this.#removeContextMenu(contextMenuId, listener) - } - } - - #registerContextMenu(id, listener) { - browser.menus.onClicked.addListener((info, tab) => { - if (info.menuItemId === id) { - listener(tab) - } - }) - } - - #addContextMenu(id, title, listener) { - browser.menus.create({ - id: id, - title: title, - contexts: [ 'tab' ] - }) - } - - #removeContextMenu(id, listener) { - browser.menus.remove(id) - } -} diff --git a/src/image-extract.js b/src/image-extract.js deleted file mode 100644 index 728989f..0000000 --- a/src/image-extract.js +++ /dev/null @@ -1,25 +0,0 @@ -import settings from '/src/settings.js' -import console from '/src/logger.js' - -export default class ImageExtract { - async extractImages(tab /* Tab */) { - try { - let js = browser.scripting.executeScript({ - target: { - tabId: tab.id, - }, - files: [ "/src/content-script.js" ], - }) - let css = browser.scripting.insertCSS({ - target: { - tabId: tab.id, - }, - files: [ "/src/content.css" ], - }) - await js - await css - } catch (error) { - console.error('Error attempting to extract images', error) - } - } -} diff --git a/src/logger.js b/src/logger.js deleted file mode 100644 index 00ed106..0000000 --- a/src/logger.js +++ /dev/null @@ -1,12 +0,0 @@ -const IS_DEBUG = false -const logger = {} - -if (IS_DEBUG) { - logger.log = console.log.bind(console) - logger.error = console.error.bind(console) -} else { - logger.log = () => {} - logger.error = () => {} -} - -export default logger diff --git a/src/main.js b/src/main.js deleted file mode 100644 index 5ab5f35..0000000 --- a/src/main.js +++ /dev/null @@ -1,105 +0,0 @@ -import settings from '/src/settings.js' -import console from '/src/logger.js' -import ContextMenus from '/src/context-menus.js' -import ImageExtract from '/src/image-extract.js' -import Shortcuts from '/src/shortcuts.js' - -let imageExtract = new ImageExtract() -let contextMenus = new ContextMenus() -let shortcuts = new Shortcuts() - -let extractTab = imageExtract.extractImages.bind(imageExtract) - -// listen for clicks on the icon to run the extract function -browser.action.onClicked.addListener(extractTab) - -contextMenus.registerContextMenu(extractTab) -shortcuts.registerKeyboardShortcuts(extractTab) - -function refreshContextMenus() { - // add a right click extract menu to tabs - contextMenus.addContextMenu(extractTab) -} - -browser.runtime.onInstalled.addListener(() => { - console.log('Installed') - refreshContextMenus() -}) - -browser.runtime.onStartup.addListener(() => { - console.log('Started') -}) - -browser.runtime.onSuspend.addListener(() => { - console.log('Suspending') -}) - -// Expose the UI settings to the content script -let getAllUISettings = async () => { - try { - const { - centerImages, - realSizeImages, - showBackgroundImages, - confirmBeforeRunningImageExtract - } = await settings.getMultipleKeyValues( - [ - 'centerImages', - 'realSizeImages', - 'showBackgroundImages', - 'confirmBeforeRunningImageExtract' - ] - ) - return { - uiSettings: true, - centerImages: centerImages, - realSizeImages: realSizeImages, - showBackgroundImages: showBackgroundImages, - confirmBeforeRunningImageExtract: confirmBeforeRunningImageExtract, - } - } catch (error) { - console.error('Failed to get UI settings', error) - } - return { uiSettings: false } -} - -let updateUISettings = async (update) => { - try { - const { - centerImages, - realSizeImages, - showBackgroundImages, - confirmBeforeRunningImageExtract - } = update - if (centerImages !== undefined) { - await settings.setKeyValue('centerImages', centerImages) - } - if (realSizeImages !== undefined) { - await settings.setKeyValue('realSizeImages', realSizeImages) - } - if (showBackgroundImages !== undefined) { - await settings.setKeyValue('showBackgroundImages', showBackgroundImages) - } - if (confirmBeforeRunningImageExtract !== undefined) { - await settings.setKeyValue('confirmBeforeRunningImageExtract', confirmBeforeRunningImageExtract) - } - return { updated: true } - } catch (error) { - console.error('Failed to update UI settings', error, update) - } - return { updated: false } -} - -browser.runtime.onMessage.addListener((data, sender) => { - // From MDN: If you only want the listener to respond to messages of a - // certain type, you must define the listener as a non-async function, - // and return a Promise only for the messages the listener is meant to - // respond to and otherwise return false or undefined: - if (data.getAllUISettings === true) { - return getAllUISettings() - } else if (data.updateUISettings === true) { - return updateUISettings(data.update) - } else { - return { uiSettings: false } - } -}) diff --git a/src/settings.js b/src/settings.js deleted file mode 100644 index 177b29d..0000000 --- a/src/settings.js +++ /dev/null @@ -1,51 +0,0 @@ -import defaults from '/settings/defaults.js' - -class Settings { - /** - * Returns the value of a key in local storage, falling back to default - * values if it is missing. - */ - async getKeyValue( - setting /* string */ - ) /* -> depends on what is stored */ { - let results = await browser.storage.local.get(setting) - if (setting in results) { - return results[setting] - } else { - return defaults[setting] - } - } - - /** - * Returns an object of values of multiple keys in local storage, falling - * back to default values if it is missing. - */ - async getMultipleKeyValues( - settings /* [string] */ - ) /* -> {depends on what is stored} */ { - let results = await browser.storage.local.get(settings) - for (const setting of settings) { - let found = false; - if (setting in results) { - found = true; - } - if (!found) { - results[setting] = defaults[setting] - } - } - return results - } - - /** - * Sets the value of a key in local storage. - */ - async setKeyValue(key, /* string */ value /* anything JSONifiable */) { - let setting = {} - setting[key] = value - await browser.storage.local.set(setting) - } -} - -let instance = new Settings() - -export default instance diff --git a/src/shortcuts.js b/src/shortcuts.js deleted file mode 100644 index d3cf8f4..0000000 --- a/src/shortcuts.js +++ /dev/null @@ -1,36 +0,0 @@ -import settings from '/src/settings.js' - -export default class Shortcuts { - async extractActiveTab(listener /* (Tab) -> () */, key /* string */) { - let shortcutEnabled = await settings.getKeyValue(key) - if (shortcutEnabled !== true) { - return - } - let tabs = await browser.tabs.query({ - active: true, - currentWindow: true - }) - // get the first (only) tab in the array to extract - let tab = tabs[0] - listener(tab) - } - - registerKeyboardShortcuts( - listener /* (Tab) -> () */, - ) { - // will be undefined on android - if (browser.commands) { - browser.commands.onCommand.addListener((command) => { - if (command === 'extract-shortcut-1') { - this.extractActiveTab(listener, 'keyboardShortcut1Enabled') - } - if (command === 'extract-shortcut-2') { - this.extractActiveTab(listener, 'keyboardShortcut2Enabled') - } - if (command === 'extract-shortcut-3') { - this.extractActiveTab(listener, 'keyboardShortcut3Enabled') - } - }) - } - } -} From 8c780951dcb0b9c94b2ef7927650a1444908166b Mon Sep 17 00:00:00 2001 From: Karaviro <131587924+Karaviro@users.noreply.github.com> Date: Fri, 16 Jan 2026 04:55:49 +0100 Subject: [PATCH 06/10] Delete settings directory --- settings/defaults.js | 9 ---- settings/manage-extension-shortcuts.png | Bin 43713 -> 0 bytes settings/settings.html | 64 ----------------------- settings/settings.js | 66 ------------------------ settings/styles.css | 18 ------- 5 files changed, 157 deletions(-) delete mode 100644 settings/defaults.js delete mode 100644 settings/manage-extension-shortcuts.png delete mode 100644 settings/settings.html delete mode 100644 settings/settings.js delete mode 100644 settings/styles.css diff --git a/settings/defaults.js b/settings/defaults.js deleted file mode 100644 index 30d4659..0000000 --- a/settings/defaults.js +++ /dev/null @@ -1,9 +0,0 @@ -export default { - centerImages : true, - realSizeImages : false, - showBackgroundImages : false, - confirmBeforeRunningImageExtract: false, - keyboardShortcut1Enabled: false, - keyboardShortcut2Enabled: false, - keyboardShortcut3Enabled: false -} diff --git a/settings/manage-extension-shortcuts.png b/settings/manage-extension-shortcuts.png deleted file mode 100644 index 259b040fa581d0af2a1d98e0809d61729037f5bd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43713 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 diff --git a/settings/settings.html b/settings/settings.html deleted file mode 100644 index a2e5a9d..0000000 --- a/settings/settings.html +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - - - - -

Default settings

- -
-
-
- - -
-
- - -
-
- - -
-
-

If set to confirm, Image Extract will display a confirmation dialog before running, allowing you to cancel if you didn't mean to. This can help prevent you from accidentally losing your tabs, but it adds an extra step.

-
-
- - -
-
-
- -

Launch settings

- -
-
- - -
- -
- - -
- -
- - -
- -

You can change the shortcut key combinations using the Manage Extension Shortcuts page

- -
- - - - diff --git a/settings/settings.js b/settings/settings.js deleted file mode 100644 index 6121056..0000000 --- a/settings/settings.js +++ /dev/null @@ -1,66 +0,0 @@ -import settings from '/src/settings.js' -import console from '/src/logger.js' - -/** - * Syncs the property checkboxes on the webpage to the default values - * of these properties from local storage or their default value. - */ -let syncPage = async (properties /* [string] */) => { - for (let property of properties) { - let value = await settings.getKeyValue(property) - document.querySelector("#" + property).checked = value - } -} - -let settingKeys = [ - 'centerImages', - 'realSizeImages', - 'showBackgroundImages', - 'confirmBeforeRunningImageExtract', - 'keyboardShortcut1Enabled', - 'keyboardShortcut2Enabled', - 'keyboardShortcut3Enabled' -] - -document.addEventListener('DOMContentLoaded', async () => { - try { - await syncPage(settingKeys) - } catch (error) { - console.error('Error syncing page to current setting values', error) - } -}) - -/** - * Syncs the corresponding local storage setting - * to this value on the page - */ -let syncLocalStorage = async (property /* string */) => { - await settings.setKeyValue(property, document.querySelector('#' + property).checked) -} - -for (let property of settingKeys) { - document.querySelector('#' + property).addEventListener('change', async () => { - try { - await syncLocalStorage(property) - } catch (error) { - console.error('Error syncing page to current setting values', error) - } - }) -} - -// will be undefined on android -if (browser.commands === undefined) { - document.addEventListener('DOMContentLoaded', () => { - // Disable settings that don't work on Android - function disable(setting) { - setting.style.opacity = 0.5 - setting.style.pointerEvents = 'none' - let text = document.createElement('strong') - text.textContent = 'Unsupported on your device/browser version' - text.style.fontSize = 'large' - text.style.color = 'darkred' - setting.prepend(text) - } - disable(document.querySelector('#commandSettings')) - }) -} diff --git a/settings/styles.css b/settings/styles.css deleted file mode 100644 index 79af51d..0000000 --- a/settings/styles.css +++ /dev/null @@ -1,18 +0,0 @@ -.setting { - display: flex; - flex-direction: row; - justify-content: flex-start; -} - -label { - font-size: 1.25rem; -} - -.reference-image { - max-height: 12em; - max-width: 100%; - object-fit: contain; - display: block; - margin-left: auto; - margin-right: auto; -} From fd4409f9f5319e41ed335d302f379509a989ebd9 Mon Sep 17 00:00:00 2001 From: Karaviro <131587924+Karaviro@users.noreply.github.com> Date: Fri, 16 Jan 2026 04:56:49 +0100 Subject: [PATCH 07/10] Delete test-webpages directory --- test-webpages/test.html | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 test-webpages/test.html diff --git a/test-webpages/test.html b/test-webpages/test.html deleted file mode 100644 index b854d2c..0000000 --- a/test-webpages/test.html +++ /dev/null @@ -1,11 +0,0 @@ -
- - - - From 41381ecfcaec007bcdc4e20ee87abe7eb055edf7 Mon Sep 17 00:00:00 2001 From: Karaviro <131587924+Karaviro@users.noreply.github.com> Date: Fri, 16 Jan 2026 04:58:59 +0100 Subject: [PATCH 08/10] Delete .gitignore --- .gitignore | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 .gitignore diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 73eb1b3..0000000 --- a/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.zip -*.project -*.settings From 16c13b9ae864c24702ba97791a56f18deca40aea Mon Sep 17 00:00:00 2001 From: Karaviro <131587924+Karaviro@users.noreply.github.com> Date: Fri, 16 Jan 2026 05:01:32 +0100 Subject: [PATCH 09/10] Add files via upload --- README.md | 83 +++++++++++++-- background.js | 139 +++++++++++++++++++++++++ icons/icon-128.png | Bin 0 -> 838 bytes icons/icon-16.png | Bin 0 -> 174 bytes icons/icon-32.png | Bin 0 -> 258 bytes icons/icon-48.png | Bin 0 -> 357 bytes icons/icon.svg | 8 ++ manifest.json | 40 ++++++++ viewer.html | 250 +++++++++++++++++++++++++++++++++++++++++++++ viewer.js | 237 ++++++++++++++++++++++++++++++++++++++++++ 10 files changed, 748 insertions(+), 9 deletions(-) create mode 100644 background.js create mode 100644 icons/icon-128.png create mode 100644 icons/icon-16.png create mode 100644 icons/icon-32.png create mode 100644 icons/icon-48.png create mode 100644 icons/icon.svg create mode 100644 manifest.json create mode 100644 viewer.html create mode 100644 viewer.js diff --git a/README.md b/README.md index 4e9217d..8dc078a 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,86 @@ -# Image Extract +# Image Extract for Chrome -Pulls images out of the current tab to show you all the images on a page when you click the icon. This is a firefox web extension: https://addons.mozilla.org/en-US/firefox/addon/image-extract/ +A minimal Chrome extension that extracts all images from the current tab and displays them in a new tab. Chrome/Edge port of [Skeletonxf's Firefox extension](https://github.com/Skeletonxf/image-extract). +## Features -Before: +- Click the extension icon or press `Alt+Shift+I` to extract images +- Finds images from `` tags, CSS backgrounds, `` elements, video posters, and SVG images +- Displays all found images in a clean dark-themed grid view +- Shows **resolution**, **format**, and **file size** for each image +- Filter images by URL +- Sort by filename +- Click any image to open it in a new window +- Download images directly -![Before running](../master/screenshots/before.png?raw=true) +## Installation -After: +### Method 1: Load unpacked (Developer Mode) -![After running](../master/screenshots/after.png?raw=true) +1. Unzip `image-extract-chrome.zip` +2. Open Chrome and go to `chrome://extensions/` +3. Enable **"Developer mode"** (toggle in top right) +4. Click **"Load unpacked"** +5. Select the `image-extract-chrome` folder +6. Done! Click the extension icon on any page to extract images. -Mobile: +### Method 2: Edge Browser -![After running](../master/screenshots/mobile-after.png?raw=true) +Works the same way in Microsoft Edge: +1. Go to `edge://extensions/` +2. Enable "Developer mode" +3. Click "Load unpacked" +4. Select the folder +## Usage + +1. Navigate to any webpage with images +2. Click the Image Extract icon in the toolbar (or press `Alt+Shift+I`) +3. A new tab opens showing all extracted images with metadata +4. Use the filter box to search for specific images +5. Click any image to open it full size, or click Download to save it + +## Fork & Contribute + +This is an unofficial Chrome port. To contribute or create your own version: + +1. **Fork the original repo**: Go to https://github.com/Skeletonxf/image-extract and click "Fork" +2. **Create a Chrome branch**: + ```bash + git checkout -b chrome-port + ``` +3. **Replace the files** with the ones from this zip +4. **Push and create a PR** (or maintain your own fork) + +Alternatively, create a new repo for the Chrome version: +1. Create a new GitHub repository +2. Extract this zip and push the contents +3. Link back to the original project in your README + +## Changelog + +### v1.1.0 +- Added resolution, format, and file size display for each image +- Fixed buttons (CSP compliance) +- Open now opens in new window while keeping viewer open +- Download actually downloads the file (falls back to open if CORS blocks) + +### v1.0.0 +- Initial Chrome port +- Manifest V3 with service worker +- Dark theme viewer with filter/sort + +## Differences from Firefox version + +- Uses Manifest V3 (Chrome's modern extension format) +- Service worker instead of background page +- Different UI with dark theme and metadata display +- Added filtering and sorting features ## License -This project is licensed under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version or version 2. +GPL-3.0 (same as original) + +## Credits + +Original Firefox extension by [Skeletonxf](https://github.com/Skeletonxf/image-extract) diff --git a/background.js b/background.js new file mode 100644 index 0000000..d5bf995 --- /dev/null +++ b/background.js @@ -0,0 +1,139 @@ +// Background service worker for Image Extract (Chrome MV3) + +chrome.action.onClicked.addListener(async (tab) => { + if (!tab.id) return; + + // Don't run on chrome:// or edge:// pages + if (tab.url?.startsWith('chrome://') || tab.url?.startsWith('edge://') || tab.url?.startsWith('about:')) { + console.log('Cannot run on browser internal pages'); + return; + } + + try { + // Inject the content script to extract images + const results = await chrome.scripting.executeScript({ + target: { tabId: tab.id }, + func: extractImages + }); + + if (results && results[0] && results[0].result) { + const imageData = results[0].result; + + // Create a new tab with the extracted images + const viewerTab = await chrome.tabs.create({ + url: chrome.runtime.getURL('viewer.html') + }); + + // Wait for the tab to load, then send the image data + chrome.tabs.onUpdated.addListener(function listener(tabId, changeInfo) { + if (tabId === viewerTab.id && changeInfo.status === 'complete') { + chrome.tabs.onUpdated.removeListener(listener); + chrome.tabs.sendMessage(viewerTab.id, { + type: 'IMAGES_DATA', + images: imageData.images, + sourceUrl: imageData.sourceUrl, + sourceTitle: imageData.sourceTitle + }); + } + }); + } + } catch (error) { + console.error('Error extracting images:', error); + } +}); + +// This function runs in the context of the web page +function extractImages() { + const images = new Set(); + + // Get all elements + document.querySelectorAll('img').forEach(img => { + // Get the actual displayed source (handles srcset, lazy loading, etc.) + const src = img.currentSrc || img.src; + if (src && !src.startsWith('data:') && src.length > 0) { + images.add(src); + } + + // Also check srcset for higher resolution versions + if (img.srcset) { + img.srcset.split(',').forEach(srcsetEntry => { + const url = srcsetEntry.trim().split(/\s+/)[0]; + if (url && !url.startsWith('data:')) { + images.add(url); + } + }); + } + }); + + // Get background images from all elements + document.querySelectorAll('*').forEach(el => { + const style = window.getComputedStyle(el); + const bgImage = style.backgroundImage; + + if (bgImage && bgImage !== 'none') { + // Extract URLs from background-image (handles multiple backgrounds) + const urlMatches = bgImage.matchAll(/url\(["']?([^"')]+)["']?\)/g); + for (const match of urlMatches) { + const url = match[1]; + if (url && !url.startsWith('data:')) { + // Resolve relative URLs + try { + const absoluteUrl = new URL(url, window.location.href).href; + images.add(absoluteUrl); + } catch (e) { + // Invalid URL, skip + } + } + } + } + }); + + // Get images from elements + document.querySelectorAll('picture source').forEach(source => { + if (source.srcset) { + source.srcset.split(',').forEach(srcsetEntry => { + const url = srcsetEntry.trim().split(/\s+/)[0]; + if (url && !url.startsWith('data:')) { + try { + const absoluteUrl = new URL(url, window.location.href).href; + images.add(absoluteUrl); + } catch (e) { + // Invalid URL, skip + } + } + }); + } + }); + + // Get images from