Pixiv 簡單存圖

透過快捷鍵與自訂名稱格式來簡單的存圖

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Pixiv easy save image
// @name:zh-TW   Pixiv 簡單存圖
// @name:zh-CN   Pixiv 简单存图
// @namespace    https://blog.maple3142.net/
// @version      0.6.3
// @description  Save pixiv image easily with custom name format and shortcut key.
// @description:zh-TW  透過快捷鍵與自訂名稱格式來簡單的存圖
// @description:zh-CN  透过快捷键与自订名称格式来简单的存图
// @author       maple3142
// @require      https://greasyfork.org/scripts/370765-gif-js-for-user-js/code/gifjs%20for%20userjs.js?version=616920
// @require      https://cdnjs.cloudflare.com/ajax/libs/jszip/3.1.5/jszip.min.js
// @require      https://unpkg.com/[email protected]/xfetch.min.js
// @require      https://unpkg.com/@snackbar/[email protected]/dist/snackbar.min.js
// @require      https://bundle.run/[email protected]
// @require      https://unpkg.com/[email protected]/gmxhr-fetch.min.js
// @require      https://gitcdn.xyz/repo/antimatter15/whammy/27ef01b3d82e9b32c7822f7a5250809e1ae89b33/whammy.js
// @match        https://www.pixiv.net/member_illust.php?mode=medium&illust_id=*
// @match        https://www.pixiv.net/
// @match        https://www.pixiv.net/bookmark.php*
// @match        https://www.pixiv.net/new_illust.php*
// @match        https://www.pixiv.net/bookmark_new_illust.php*
// @match        https://www.pixiv.net/ranking.php*
// @match        https://www.pixiv.net/search.php*
// @match        https://www.pixiv.net/artworks*
// @match        https://www.pixiv.net/member.php*
// @connect      pximg.net
// @grant        GM_xmlhttpRequest
// @compatible   firefox >=52
// @compatible   chrome >=55
// @license      MIT
// ==/UserScript==

;(function() {
	'use strict'
	const FORMAT = {
		single: d => `${d.title}-${d.userName}-${d.id}`,
		multiple: (d, i) => `${d.title}-${d.userName}-${d.id}-p${i}`
	}
	const KEYCODE_TO_SAVE = 83 // 83 is 's' key
	const SAVE_UGOIRA_AS_WEBM = false // faster than gif
	const USE_PIXIVCAT = true // much faster than pximg

	const gxf = xf.extend({ fetch: gmfetch })
	const $ = s => document.querySelector(s)
	const $$ = s => [...document.querySelectorAll(s)]
	const elementmerge = (a, b) => {
		Object.keys(b).forEach(k => {
			if (typeof b[k] === 'object') elementmerge(a[k], b[k])
			else if (k in a) a[k] = b[k]
			else a.setAttribute(k, b[k])
		})
	}
	const $el = (s, o = {}) => {
		const el = document.createElement(s)
		elementmerge(el, o)
		return el
	}
	const download = (url, fname) => {
		const a = $el('a', { href: url, download: fname || true })
		document.body.appendChild(a)
		a.click()
		document.body.removeChild(a)
	}
	const downloadBlob = (blob, fname) => {
		const url = URL.createObjectURL(blob)
		download(url, fname)
		URL.revokeObjectURL(url)
	}
	const blobToCanvas = blob =>
		new Promise((res, rej) => {
			const src = URL.createObjectURL(blob)
			const img = $el('img', { src })
			const cvs = $el('canvas')
			const ctx = cvs.getContext('2d')
			img.onload = () => {
				URL.revokeObjectURL(src)
				cvs.height = img.naturalHeight
				cvs.width = img.naturalWidth
				ctx.drawImage(img, 0, 0)
				res(cvs)
			}
			img.onerror = e => {
				URL.revokeObjectURL(src)
				rej(e)
			}
		})
	const getJSONBody = url => xf.get(url).json(r => r.body)
	const getIllustData = id => getJSONBody(`/ajax/illust/${id}`)
	const getUgoiraMeta = id => getJSONBody(`/ajax/illust/${id}/ugoira_meta`)
	const getCrossOriginBlob = (url, Referer = 'https://www.pixiv.net/') =>
		gxf.get(url, { headers: { Referer } }).blob()
	const getImageFromPximg = (url, pixivcat_multiple_systax) => {
		if (USE_PIXIVCAT) {
			const [_, id, idx] = /\/(\d+)_p(\d+)/.exec(url)
			const newUrl = pixivcat_multiple_systax
				? `https://pixiv.cat/${id}-${parseInt(idx) + 1}.png`
				: `https://pixiv.cat/${id}.png`
			return xf.get(newUrl).blob()
		}
		return getCrossOriginBlob(url)
	}
	const saveImage = async ({ single, multiple }, id) => {
		const illustData = await getIllustData(id)
		if (snackbar) {
			snackbar.createSnackbar(`Downloading ${illustData.title}...`, {
				timeout: 1000
			})
		}
		let results
		const { illustType } = illustData
		switch (illustType) {
			case 0:
			case 1:
				{
					// normal
					const url = illustData.urls.original
					const ext = url
						.split('/')
						.pop()
						.split('.')
						.pop()
					if (illustData.pageCount === 1) {
						results = [[single(illustData) + '.' + ext, await getImageFromPximg(url)]]
					} else {
						const len = illustData.pageCount
						const ar = []
						for (let i = 0; i < len; i++) {
							ar.push(
								Promise.all([
									multiple(illustData, i) + '.' + ext,
									getImageFromPximg(url.replace('p0', `p${i}`), true)
								])
							)
						}
						results = await Promise.all(ar)
					}
				}
				break
			case 2: {
				// ugoira
				const fname = single(illustData)
				const ugoiraMeta = await getUgoiraMeta(id)
				const ugoiraZip = await xf.get(ugoiraMeta.originalSrc).blob()
				const { files } = await JSZip.loadAsync(ugoiraZip)
				const frames = await Promise.all(Object.values(files).map(f => f.async('blob').then(blobToCanvas)))
				if (SAVE_UGOIRA_AS_WEBM) {
					const getWebm = (data, frames) =>
						new Promise((res, rej) => {
							const encoder = new Whammy.Video()
							for (let i = 0; i < frames.length; i++) {
								encoder.add(frames[i], data.frames[i].delay)
							}
							encoder.compile(false, res)
						})
					results = [[fname + '.webm', await getWebm(ugoiraMeta, frames)]]
				} else {
					const numCpu = navigator.hardwareConcurrency || 4
					const getGif = (data, frames) =>
						new Promise((res, rej) => {
							const gif = new GIF({ workers: numCpu * 4, quality: 10 })
							for (let i = 0; i < frames.length; i++) {
								gif.addFrame(frames[i], { delay: data.frames[i].delay })
							}
							gif.on('finished', x => {
								res(x)
							})
							gif.on('error', rej)
							gif.render()
						})
					results = [[fname + '.gif', await getGif(ugoiraMeta, frames)]]
				}
			}
		}

		// `filenamify` will normalize file names, since some character is not allowed
		if (results.length === 1) {
			const [f, blob] = results[0]
			downloadBlob(blob, filenamify(f))
		} else {
			const zip = new JSZip()
			for (const [f, blob] of results) {
				zip.file(filenamify(f), blob)
			}
			const blob = await zip.generateAsync({ type: 'blob' })
			const zipname = single(illustData)
			downloadBlob(blob, filenamify(zipname))
		}
	}

	// key shortcut
	function getSelector() {
		const SELECTOR_MAP = {
			'/': 'a.work:hover,a._work:hover,.illust-item-root>a:hover',
			'/bookmark\\.php': 'a.work:hover',
			'/new_illust\\.php': 'a.work:hover',
			'/bookmark_new_illust\\.php': 'figure>div>a:hover,.illust-item-root>a:hover',
			'/artworks/\\d+': 'div[role=presentation]>a:hover,canvas:hover',
			'/ranking\\.php': 'a.work:hover,.illust-item-root>a:hover',
			'/search\\.php': 'figure>div>a:hover',
			'/member\\.php': '[href^="/artworks"]:hover,.illust-item-root>a:hover'
		}
		for (const [key, val] of Object.entries(SELECTOR_MAP)) {
			const rgx = new RegExp(`^${key}$`)
			if (rgx.test(location.pathname)) {
				return val
			}
		}
	}
	{
		addEventListener('keydown', e => {
			if (e.which !== KEYCODE_TO_SAVE) return
			e.preventDefault()
			e.stopPropagation()
			const selector = getSelector()
			let id
			if ($('#Patchouli')) {
				const el = $('.image-item-image:hover>a')
				if (el) id = /\d+/.exec(el.href.split('/').pop())[0]
			}
			if (!id && typeof selector === 'string') {
				const el = $(selector)
				if (el && el.href) id = /\d+/.exec(el.href.split('/').pop())[0]
				else if (location.pathname.startsWith('/artwork')) id = location.pathname.split('/').pop()
			}
			if (id) saveImage(FORMAT, id).catch(console.error)
		})
	}
	{
		document.body.appendChild(
			$el('link', {
				rel: 'stylesheet',
				href: 'https://unpkg.com/@snackbar/[email protected]/dist/snackbar.min.css'
			})
		)
	}
})()