Google DWIMages

Direct links to images and pages on Google Images

当前为 2020-11-29 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name          Google DWIMages
// @description   Direct links to images and pages on Google Images
// @author        chocolateboy
// @copyright     chocolateboy
// @version       2.3.0
// @namespace     https://github.com/chocolateboy/userscripts
// @license       GPL: https://www.gnu.org/copyleft/gpl.html
// @include       https://www.google.tld/*tbm=isch*
// @include       https://encrypted.google.tld/*tbm=isch*
// @require       https://cdn.jsdelivr.net/npm/[email protected]/dist/cash.min.js
// @require       https://unpkg.com/[email protected]/dist/index.iife.min.js
// @grant         GM_log
// @inject-into   auto
// ==/UserScript==

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

const SELECTOR = 'div[data-ri][data-ved][jsaction]'

// events to intercept (stop propagating) in result elements
const EVENTS = 'click focus mousedown'

let METADATA

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

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

        if (!isImageDataRequest(method, url)) {
            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
                $container.children(SELECTOR).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 (method, url) {
    return method.toUpperCase() === 'POST' && /\/batchexecute\?rpcids=/.test(url)
}

// return the URL for the nth image (0-based) and remove its data from the tree
function nthImageUrl (index) {
    const result = METADATA[index][1][3][0]
    delete METADATA[index]
    return result
}

// 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 embedded in the page and register
// a listener for the requests for additional data
function init () {
    const $container = $('.islrc')

    if (!$container.length) {
        throw new Error("Can't find results container")
    }

    const initialMetadata = imageMetadata(GMCompat.unsafeWindow.AF_initDataChunkQueue[1].data)

    // clone the data so we can mutate it (remove obsolete entries)
    //
    // XXX this (or at least a shallow clone) is also needed to avoid permission
    // errors in Violentmonkey for Firefox (but not Violentmonkey for Chrome)
    METADATA = JSON.parse(JSON.stringify(initialMetadata))

    // there's static data for the first ~100 images, but only the first 50 are
    // shown initially. the next 50 are loaded dynamically and then the
    // remaining images are loaded in batches of 100 (these can be processed
    // synchronously because the images have been added to the DOM by the time
    // the data arrives)
    const callback = (mutations, observer) => {
        const $elements = $container.children(SELECTOR)

        for (const el of $elements) {
            const index = $(el).data('ri')

            if (index < METADATA.length) {
                onResult.call(el)
            } else {
                observer.disconnect()
                break
            }
        }
    }

    // process the initial images
    const $initial = $container.children(SELECTOR)
    const observer = new MutationObserver(callback)
    const xhrProto = GMCompat.unsafeWindow.XMLHttpRequest.prototype

    $initial.each(onResult)
    observer.observe($container.get(0), { childList: true })
    xhrProto.open = GMCompat.export(hookXhrOpen(xhrProto.open, $container))
}

// 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 result:", index)
        return // continue
    }

    // prevent new trackers being registered on this DIV and its descendant
    // elements
    //
    // XXX cash doesn't support +addBack+
    $result.find('*').add($result).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(EVENTS, stopPropagation)
    $imageLink.on(EVENTS, stopPropagation)

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

try {
    init()
} catch (e) {
    console.error('Initialisation error:', e)
}