NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Google Images All Sizes // @version 1.11.0 // @description Adds 'All Sizes' and 'Full Size' buttons to google images. // @icon https://www.google.com/s2/favicons?sz=64&domain=google.com // @license MIT // @author Nour Nasser // @namespace https://github.com/Nourz1234 // @match *://www.google.com/search*udm=2* // @match *://www.google.com/search*tbm=isch* // @match *://www.google.com/imgres* // @run-at document-end // @updateURL https://raw.githubusercontent.com/Nourz1234/google-images-all-sizes/master/dist/google-images-all-sizes.js // @downloadURL https://raw.githubusercontent.com/Nourz1234/google-images-all-sizes/master/dist/google-images-all-sizes.js // @supportURL https://github.com/Nourz1234/google-images-all-sizes/issues // @connect * // @grant GM.xmlHttpRequest // @grant GM_addStyle // ==/UserScript== (function () { const SVGTags = { animate: null, circle: null, clipPath: null, defs: null, desc: null, ellipse: null, feBlend: null, feColorMatrix: null, feComponentTransfer: null, feComposite: null, feConvolveMatrix: null, feDiffuseLighting: null, feDisplacementMap: null, feDistantLight: null, feFlood: null, feFuncA: null, feFuncB: null, feFuncG: null, feFuncR: null, feGaussianBlur: null, feImage: null, feMerge: null, feMergeNode: null, feMorphology: null, feOffset: null, fePointLight: null, feSpecularLighting: null, feSpotLight: null, feTile: null, feTurbulence: null, filter: null, foreignObject: null, g: null, image: null, line: null, linearGradient: null, marker: null, mask: null, metadata: null, path: null, pattern: null, polygon: null, polyline: null, radialGradient: null, rect: null, stop: null, svg: null, switch: null, symbol: null, text: null, textPath: null, tspan: null, use: null, view: null, }; function createElement(tag, props, ...children) { // console.info(tag, props, children); let elem; if (tag in SVGTags) { elem = document.createElementNS("http://www.w3.org/2000/svg", tag); } else { elem = document.createElement(tag); } setProps(elem, props || {}); appendChildren(elem, children); return elem; } function setProps(elem, props) { Object.entries(props).forEach(([key, value]) => { if (key === "style" && value instanceof Object) { Object.assign(elem.style, value); } else if (key === "dataset" && value instanceof Object) { Object.assign(elem.dataset, value); } else if (hasKey(elem, key) && !isKeyReadonly(elem, key)) { elem[key] = value; } else { elem.setAttribute(key, String(value)); } }); } function appendChildren(elem, children) { children.flat().forEach(child => { if (child instanceof Node) { elem.appendChild(child); } else if (child !== null && child !== false) { elem.appendChild(document.createTextNode(String(child))); } }); } function createDocumentFromHTML(html) { let doc = document.implementation.createHTMLDocument(""); doc.open(); doc.write(html); doc.close(); return doc; } function isElementVisible(elem) { return elem.offsetParent !== null && elem.style.visibility != "hidden"; } function isKeyReadonly(obj, key) { let currentObj = obj; while (currentObj !== null) { const desc = Object.getOwnPropertyDescriptor(currentObj, key); if (desc) { return desc.writable === false || desc.set === undefined; } currentObj = Object.getPrototypeOf(currentObj); } return true; } function hasKey(obj, key) { return key in obj; } logInfo("Started!"); GM_addStyle(getFile("styles.css")); const observer = new MutationObserver(addButtons); observer.observe(document.body, { attributes: true, subtree: true }); function getPreviewImageUrl(source) { const imgs = source.closest("[data-tbnid]") ?.querySelectorAll("a > img"); return Array.from(imgs || []).find(isElementVisible)?.src; } function getImageDetails(source) { const container = source.closest('div[data-sid]'); if (!container) { throw new Error("Failed to find image container"); } const query = container.dataset.query; const tbnId = container.dataset.sid; if (!tbnId) { throw new Error("Failed to fetch image tbn id"); } const docId = document.querySelector(`[data-docid="${tbnId}"]`)?.dataset.refDocid; return { query, docId, tbnId }; } async function getAllSizesForImage(source) { const { query, docId, tbnId } = getImageDetails(source); const url = new URL('/search', window.location.href); url.searchParams.append("q", query || ""); url.searchParams.append("tbm", "isch"); url.searchParams.append("docid", docId || ""); url.searchParams.append("tbnid", tbnId); url.searchParams.append("tbs", "simg:m00"); // logInfo(url.href); const response = await GM.xmlHttpRequest({ method: "GET", url: url.href, headers: { "User-Agent": "Mozilla/5.0 (Android 7.0; Mobile; rv:60.0) Gecko/60.0 Firefox/60.0", // important! } }); const doc = createDocumentFromHTML(response.responseText); return Array.from(doc.querySelectorAll("[data-docid]")).map(elem => { const img = elem.querySelector("img"); return { docId: elem.dataset.docid, tbnId: elem.dataset.tbnid, width: elem.dataset.ow, height: elem.dataset.oh, src: elem.dataset.ou, previewSrc: img.dataset.src || img.src, alt: img.alt, site: elem.dataset.st, url: elem.dataset.ru, title: elem.dataset.pt, }; }); } function addButtons() { let btnBars = document.querySelectorAll('div[class="HJRshd"]'); for (let btnBar of btnBars) { const btnOpenImageInFullSize = btnBar.querySelector('#gias_fullSize'); const btnViewAllSizes = btnBar.querySelector('#gias_allSizes'); if (btnOpenImageInFullSize !== null || btnViewAllSizes !== null) return; btnBar.insertAdjacentElement('afterbegin', renderMainButtons()); } } function showImagesModal(images) { document.body.appendChild(renderImagesModal(images)); } async function openImage(image, doNotOpenExternalWebsites) { logInfo("openImage", image); let response = await GM.xmlHttpRequest({ method: 'HEAD', url: image.src, }); const isImage = response.responseHeaders.toLowerCase().includes("content-type: image/"); if (!isImage) { window.open(doNotOpenExternalWebsites ? image.previewSrc : image.src, "_blank"); return; } response = await GM.xmlHttpRequest({ method: 'GET', url: image.src, responseType: "blob", }); window.open(URL.createObjectURL(response.response), "_blank"); } function renderMainButtons() { function onViewFullSize() { let imgUrl = getPreviewImageUrl(this); if (imgUrl !== null) window.open(imgUrl, '_blank'); } async function onViewAllSizes() { this.classList.add("loading"); this.disabled = true; try { const images = await getAllSizesForImage(this); // logInfo(images); showImagesModal(images); this.disabled = false; } catch (e) { handleError(e); } finally { this.classList.remove("loading"); } } return (createElement("div", { style: { display: 'flex', gap: "1em" } }, createElement("button", { id: "gias_fullSize", className: "btn", onclick: onViewFullSize }, "Full Size"), createElement("button", { id: "gias_allSizes", className: "btn", onclick: onViewAllSizes }, "All Sizes"))); } function renderImagesModal(images) { const context = { doNotOpenExternalWebsites: true, }; logInfo(images); const modal = (createElement("div", { className: "gias-modal", onclick: function (e) { if (e.target == this) this.remove(); } }, createElement("div", { className: "modal-content" }, createElement("div", { className: "modal-head" }, createElement("div", { className: "title" }, "All Sizes"), createElement("label", { title: "If the full size image link does not lead to an actual image then open the preview image instead of opening an external website.", htmlFor: "doNotOpenExternalWebsites" }, createElement("input", { id: "doNotOpenExternalWebsites", name: "doNotOpenExternalWebsites", type: "checkbox", checked: context.doNotOpenExternalWebsites, onchange: (e) => context.doNotOpenExternalWebsites = e.target.checked }), "Don't open external websites"), createElement("button", { className: "close", type: "button", onclick: function () { this.closest(".gias-modal")?.remove(); } }, createElement("svg", { width: "24", height: "24", viewBox: "0 0 24 24" }, createElement("path", { d: "M 4.9902344 3.9902344 A 1.0001 1.0001 0 0 0 4.2929688 5.7070312 L 10.585938 12 L 4.2929688 18.292969 A 1.0001 1.0001 0 1 0 5.7070312 19.707031 L 12 13.414062 L 18.292969 19.707031 A 1.0001 1.0001 0 1 0 19.707031 18.292969 L 13.414062 12 L 19.707031 5.7070312 A 1.0001 1.0001 0 0 0 18.980469 3.9902344 A 1.0001 1.0001 0 0 0 18.292969 4.2929688 L 12 10.585938 L 5.7070312 4.2929688 A 1.0001 1.0001 0 0 0 4.9902344 3.9902344 z" })))), createElement("div", { className: "modal-body" }, createElement("div", { className: "img-panel" }, images.length && images.map(img => renderImage(img, context)), !images.length && createElement("div", null, "No images found.")))))); return modal; } function renderImage(image, context) { async function onOpenImage() { this.classList.add("loading"); this.disabled = true; try { await openImage(image, context.doNotOpenExternalWebsites); } catch (error) { logError("Error opening image", error); window.open(image.src, "_blank"); } finally { this.classList.remove("loading"); this.disabled = false; } } function onOpenInGoogle() { const url = new URL("/imgres", window.location.href); // url.searchParams.set("q", ""); // url.searchParams.set("imgurl", image.src); url.searchParams.set("imgrefurl", image.url); url.searchParams.set("docid", image.docId); url.searchParams.set("tbnid", image.tbnId); // url.searchParams.set("w", image.width); // url.searchParams.set("h", image.height); window.open(url.href, "_blank"); } function openWebsite() { window.open(image.url, "_blank"); } return (createElement("div", { className: "img-container" }, createElement("div", { className: "img" }, createElement("img", { loading: "lazy", src: image.previewSrc, alt: image.alt }), createElement("div", { className: "overlay" }, createElement("div", { className: "actions-container" }, createElement("div", { className: "actions" }, createElement("button", { className: "btn", onclick: onOpenImage }, "Open"), createElement("button", { className: "btn", onclick: onOpenInGoogle }, "Open in Google"), createElement("button", { className: "btn", onclick: openWebsite }, "Open Website"))), createElement("div", { className: "img-size" }, image.width, "x", image.height))), createElement("div", { className: "img-info" }, createElement("a", { className: "site", href: image.url, target: "_blank", title: image.site }, image.site), createElement("span", { className: "title", title: image.title }, image.title)))); } function logInfo(...data) { console.info("[Google Images All Sizes]", ...data); } function logError(...data) { console.error("[Google Images All Sizes]", ...data); } function showMsg(message) { window.alert(`[Google Images All Sizes]:\n${message}`); } function handleError(...data) { logError(...data); showMsg("An unhandled error occurred.\nCheck console for details."); } function getFile(name) { const files = { 'styles.css': () => ` /* variables */ :root { --clr-text-light: #e4e4e4; --clr-text-dark: #242424; /* Support for light mode? What is this? Did I really need to add this? */ --clr-background: white; --clr-background2: lightgray; --clr-background-disabled: #424242; --clr-border: #cfcfcf; --clr-text: var(--clr-text-dark); --clr-text-disabled: #6e6e6e; --clr-gray-text: #9e9e9e; --clr-accent: #0060df; @media (prefers-color-scheme: dark) { --clr-background: #1f1f1f; --clr-background2: #292929; /* --clr-background-disabled: #424242; */ --clr-border: #3f3f3f; --clr-text: var(--clr-text-light); /* --clr-text-disabled: #6e6e6e; */ /* --clr-gray-text: #9e9e9e; */ /* --clr-accent: #0060df; */ } --img-height: 200px; --img-min-width: 100px; --img-max-width: 356px; --gap: 0.5em; /* use system accent color if available */ @supports (color: AccentColor) { --clr-accent: AccentColor; } } /* The Modal (background) */ .gias-modal { all: revert; position: fixed; display: flex; flex-flow: row; justify-content: center; left: 0; top: 0; width: 100vw; height: 100vh; z-index: 10000; background: rgba(0, 0, 0, 0.6); font-family: Arial; font-size: medium; accent-color: var(--clr-accent); color: var(--clr-text); * { transition: all 0.3s ease-out; } a { color: var(--clr-gray-text); font-size: small; } /* Modal Content */ .modal-content { background: var(--clr-background); border: 1px solid var(--clr-border); border-radius: 1em; margin: 3em; padding: var(--gap); display: flex; flex-flow: column; } /* Modal Header */ .modal-head { display: flex; flex-flow: row; justify-content: space-between; align-items: center; gap: 3em; border: 0; border-bottom: 1px solid var(--clr-border); .title { font-size: large; margin-left: 0.3em; } } /* Modal Body */ .modal-body { display: flex; justify-content: center; overflow: auto; padding-top: 1em; } .img-panel { display: flex; flex-flow: row; flex-wrap: wrap; justify-content: center; gap: var(--gap); } .img-container { display: flex; flex-flow: column; gap: var(--gap); } .img { min-width: var(--img-min-width); max-width: var(--img-max-width); height: var(--img-height); border-radius: 1em; overflow: hidden; display: flex; flex-flow: column; justify-content: center; background: var(--clr-background2); position: relative; filter: drop-shadow(0px 2px 5px black); img { max-width: 100%; max-height: 100%; object-fit: contain; } } .overlay { position: absolute; width: 100%; height: 100%; display: flex; flex-flow: column; } .actions-container { display: flex; flex-flow: column; justify-content: center; align-items: center; flex-grow: 1; opacity: 0; background: rgba(0, 0, 0, 0.6); } .actions-container:hover { opacity: 1; } .actions { display: flex; flex-flow: column; width: max-content; gap: var(--gap); } .img-size { text-align: center; color: var(--clr-text-light); background: rgba(0, 0, 0, 0.6); } .img-info { display: flex; flex-flow: column; /* start at 0 */ width: 0px; /* grow to fill available space (i don't know how it works, but it does ¯\\_(ツ)_/¯) */ min-width: 100%; white-space: nowrap; .site, .title { overflow: hidden; text-overflow: ellipsis; } .site { text-decoration: none; } .site:hover { text-decoration: underline; } } .close { align-self: flex-start; fill: var(--clr-gray-text); border: none; background: none; cursor: pointer; } .close:hover { filter: brightness(125%); } .gap { gap: var(--gap); } .d-flex { display: flex; align-items: center; } } .btn { text-decoration: none; color: var(--clr-text-light); background: var(--clr-accent); padding: 0.5em; box-sizing: border-box; border: none; border-radius: 1em; flex-grow: 1; cursor: pointer; /* overflow: hidden; */ position: relative; } .btn:hover { filter: brightness(125%); } .btn:disabled { background: var(--clr-background-disabled); color: var(--clr-text-disabled); pointer-events: none; } .loading::before { content: ''; position: absolute; pointer-events: none; top: 50%; left: 50%; transform: translate(-50%, -50%); aspect-ratio: 1; height: 25px; width: auto; background: var(--clr-background); border-radius: 50%; } .loading::after { content: ''; position: absolute; pointer-events: none; top: 50%; left: 50%; transform: translate(-50%, -50%); aspect-ratio: 1; height: 25px; width: auto; box-sizing: border-box; border: 4px solid transparent; border-top: 4px solid #007bff; border-radius: 50%; animation: spin 1s linear infinite; } @keyframes spin { 0% { transform: translate(-50%, -50%) rotate(0deg); } 100% { transform: translate(-50%, -50%) rotate(360deg); } } `, }; return files[name](); } })();