Google DWIMages

Direct links to images and pages on Google Images

目前為 2020-03-25 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name          Google DWIMages
// @description   Direct links to images and pages on Google Images
// @author        chocolateboy
// @copyright     chocolateboy
// @version       2.1.0
// @namespace     https://github.com/chocolateboy/userscripts
// @license       GPL: http://www.gnu.org/copyleft/gpl.html
// @include       https://www.google.tld/*tbm=isch*
// @include       https://encrypted.google.tld/*tbm=isch*
// @require       https://code.jquery.com/jquery-3.4.1.min.js
// @grant         GM_log
// @inject-into   content
// ==/UserScript==

// XXX note: the unused grant is a workaround for a Greasemonkey bug:
// https://github.com/greasemonkey/greasemonkey/issues/1614

let METADATA

/******************************** helper functions ****************************/

// extract the image metadata for the original batch of results from the
// content of the SCRIPT tag
function extractMetadata (source) {
    // XXX not all browsers support the ES2018 /s (matchAll) flag
    // const json = source.match(/(\[.+\])/s)[1]
    const json = source.match(/(\[[\s\S]+\])/)[1]
    return JSON.parse(json)
}

// return a wrapper for XmlHttpRequest#open which intercepts image-metadata
// requests and appends the results to our metadata store (array)
function hookXHROpen (oldOpen) {
    return function open (...args) {
        oldOpen.apply(this, args) // there's no return value

        if (!isImageDataRequest(args)) {
            return
        }

        // a new XHR instance is created for each metadata request, so we need
        // to register a new listener
        this.addEventListener('load', () => {
            let parsed

            try {
                const cooked = this.responseText.match(/"\[[\s\S]+\](?:\\n)?"/)[0] // '"[...]\n"'
                const raw = JSON.parse(cooked) // '[...]'
                parsed = JSON.parse(raw) // [...]
            } catch (e) {
                console.error("Can't parse response:", e)
                return
            }

            try {
                METADATA = METADATA.concat(imageMetadata(parsed))
                // process the new images
                $('div[data-ri][data-ved][jsaction]').each(onResult)
            } catch (e) {
                console.error("Can't merge new metadata:", e)
            }
        })
    }
}

// return the image metadata subtree (array) of the full metadata tree
function imageMetadata (tree) {
    return tree[31][0][12][2]
}

// determine whether an XHR request is an image-metadata request
function isImageDataRequest (args) {
    return (args.length >= 2)
        && (args[0].toUpperCase() === 'POST')
        && /\/batchexecute\?rpcids=/.test(args[1])
}

// return the URL for the nth image (0-based)
function nthImageUrl (index) {
    return METADATA[index][1][3][0]
}

// event handler for image links, page links and results which prevents their
// click/mousedown events being hijacked for tracking
function stopPropagation (e) {
    e.stopPropagation()
}

/************************************* main ************************************/

// extract the data for the first ≈ 100 images out of the SCRIPT element
// embedded in the page and register a listener for the requests for
// additional data
function init () {
    const scripts = Array.from(document.scripts)
    const callbacks = scripts.filter(script => /^AF_initDataCallback\b/.test(script.text))
    const callback = callbacks.pop().text

    METADATA = imageMetadata(extractMetadata(callback))
    window.XMLHttpRequest.prototype.open = hookXHROpen(window.XMLHttpRequest.prototype.open)
}

// process an image result (DIV), assigning the image URL to its first link and
// disabling trackers
//
// used to process the original batch of results as well as the lazily-loaded
// updates
function onResult () {
    // grab the metadata for this result
    const $result = $(this)
    const index = $result.data('ri') // 0-based index of the result

    let imageUrl

    try {
        imageUrl = nthImageUrl(index)
    } catch (e) {
        console.warn(`Can't find image URL for image #${index + 1}`)
        return // continue
    }

    // prevent new trackers being registered on this DIV and its descendant
    // elements
    $result.find('*').addBack().removeAttr('jsaction')

    // assign the correct/missing URI to the image link
    const $links = $result.find('a')
    const $imageLink = $links.eq(0)
    const $pageLink = $links.eq(1)

    $imageLink.attr('href', imageUrl)

    // pre-empt the existing trackers on elements which don't already have
    // direct listeners (the result element and the image link)
    $result.on('click focus mousedown', stopPropagation)
    $imageLink.on('click focus mousedown', stopPropagation)

    // forcibly remove trackers from the remaining element (the page link)
    $pageLink.replaceWith($pageLink.clone())
}

try {
    init()
    // process the initial images
    $('div[data-ri][data-ved]').each(onResult)
} catch (e) {
    console.error("Can't parse metadata:", e)
}