您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
이미지로부터 Exif 정보를 추출해 사용자에게 보여줍니다
当前为
- // ==UserScript==
- // @name prompt-extractor.user.js
- // @namespace https://github.com/toriato/userscripts/prompt-extractor.user.js
- // @version 0.1.0
- // @description 이미지로부터 Exif 정보를 추출해 사용자에게 보여줍니다
- // @author Sangha Lee <totoriato@gmail.com>
- // @license MIT
- // @match https://arca.live/b/*
- // @run-at document-start
- // @grant GM_xmlhttpRequest
- // @grant GM_addStyle
- // @require https://cdn.jsdelivr.net/npm/exifreader@4.9.2/dist/exif-reader.min.js
- // ==/UserScript==
- GM_addStyle(/*css*/`
- @keyframes spin {
- from { transform:rotate(0deg) }
- to { transform:rotate(360deg) }
- }
- figure.params {
- margin: 0;
- position: relative;
- display: table;
- }
- figure.params img {
- max-width: 100%;
- }
- /* 우측 상단 상태 아이콘 */
- figure.params:not([data-params=""])::after {
- position: absolute;
- right: 0;
- top: 0;
- margin: .5em;
- font-size: 2rem;
- text-shadow: 0 0 4px black;
- content: '❤️'
- }
- figure.params.loading::after {
- animation: spin 1s infinite linear;
- content: '🌀'
- }
- figure.params:not(.loading):not([data-params=""]):hover::after {
- display: none;
- }
- figure.params figcaption {
- transition: transform .25s, opacity .25s;
- transform: scaleY(0);
- transform-origin: top;
- position: absolute;
- left: 0;
- top: 0;
- overflow-y: auto;
- max-height: 50%;
- padding: .5em;
- opacity: 0;
- background-color: rgba(0, 0, 0, 0.5);
- text-align: left;
- pointer-events: none;
- }
- figure.params:not(.loading):not([data-params=""]):hover figcaption {
- transform: scaleY(1);
- opacity: 1;
- pointer-events: inherit;
- }
- `)
- /**
- * UPNG.js - JS PNG Decoder/Encoder
- * https://github.com/photopea/UPNG.js
- * MIT License
- */
- class UPNG {
- static bin = {
- nextZero: (data, p) => {
- while (data[p] != 0) p++
- return p
- },
- readUshort: (buff, p) =>
- (buff[p] << 8) | buff[p + 1],
- writeUshort: (buff, p, n) => {
- buff[p] = (n >> 8) & 255
- buff[p + 1] = n & 255
- },
- readUint: (buff, p) =>
- (buff[p] * (256 * 256 * 256)) + ((buff[p + 1] << 16) | (buff[p + 2] << 8) | buff[p + 3]),
- writeUint: (buff, p, n) => {
- buff[p] = (n >> 24) & 255
- buff[p + 1] = (n >> 16) & 255
- buff[p + 2] = (n >> 8) & 255
- buff[p + 3] = n & 255
- },
- readASCII: (buff, p, l) => {
- let s = ''
- for (let i = 0; i < l; i++)
- s += String.fromCharCode(buff[p + i])
- return s
- },
- writeASCII: (data, p, s) => {
- for (let i = 0; i < s.length; i++)
- data[p + i] = s.charCodeAt(i)
- },
- readBytes: (buff, p, l) => {
- const arr = []
- for (let i = 0; i < l; i++)
- arr.push(buff[p + i]);
- return arr
- },
- pad: (n) =>
- n.length < 2 ? '0' + n : n,
- readUTF8: function (buff, p, l) {
- let s = ''
- let ns;
- for (var i = 0; i < l; i++) s += "%" + UPNB.bin.pad(buff[p + i].toString(16));
- try { ns = decodeURIComponent(s); }
- catch (e) { return UPNG.bin.readASCII(buff, p, l); }
- return ns;
- }
- }
- static magicNumbers = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]
- static decode(buff) {
- const bin = UPNG.bin
- const data = new Uint8Array(buff)
- const texts = {}
- for (let i = 0; i < 8; i++) {
- if (data[i] !== UPNG.magicNumbers[i]) {
- throw Error('Input file is not a PNG')
- }
- }
- let offset = 8
- while (offset < data.length) {
- const len = bin.readUint(data, offset); offset += 4;
- const type = bin.readASCII(data, offset, 4); offset += 4;
- if (type == 'tEXt') {
- const nz = bin.nextZero(data, offset);
- const key = bin.readASCII(data, offset, nz - offset);
- const textLen = offset + len - nz - 1;
- texts[key] = bin.readASCII(data, nz + 1, textLen)
- }
- offset += len + 4;
- }
- return texts
- }
- }
- /**
- * @param {UIEvent} event
- */
- function onLoad(event) {
- /** @type {HTMLImageElement} */
- const node = event.target
- // 작은 이미지는 메타데이터 확인하지 않기
- const rect = node.getBoundingClientRect()
- if (rect.width < 128 || rect.height < 128) {
- return
- }
- let src = new URL(node.src)
- // 아카라이브에선 원본 이미지에만 Exif 데이터가 존재함
- if (src.host.endsWith('namu.la')) {
- src.searchParams.set('type', 'orig')
- }
- const $figure = document.createElement('figure')
- $figure.classList.add('params', 'loading')
- $figure.innerHTML = /*html*/`
- ${node.closest('p').innerHTML}
- <figcaption></figcaption>
- `
- node.replaceWith($figure)
- // Exif 로부터 파라미터 문자열 가져오기
- let params = ''
- new Promise((resolve, reject) => {
- GM_xmlhttpRequest({
- url: src.toString(),
- responseType: 'arraybuffer',
- onload: resolve,
- onerror: reject
- })
- })
- .then(res => {
- const headers = Object.fromEntries(
- res.responseHeaders
- .split(/\r?\n/)
- .map(v => {
- const [key, value] = v.split(':', 2).map(v => v.trim())
- return [key.toLowerCase(), value]
- })
- )
- const contentType = headers['content-type']
- switch (contentType) {
- case 'image/png':
- const texts = UPNG.decode(res.response)
- params = texts['parameters'] ?? ''
- break
- default:
- // 반환 받은 파일이 이미지가 아니라면 무시하기
- if (!contentType.startsWith('image/')) {
- return
- }
- try {
- const tags = ExifReader.load(res.response)
- if (tags?.UserComment?.value) {
- params = String.fromCharCode(
- // 첫 8바이트는 인코딩 타입이므로 디코딩 할 필요 없음
- // https://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif/usercomment.html
- ...tags.UserComment.value.slice(8).filter(v => v !== 0)
- )
- }
- } catch (err) {
- // 메타데이터가 존재하지 않는 이미지라면 무시하기
- if (err.name !== 'MetadataMissingError') {
- throw err
- }
- }
- }
- })
- .finally(() => {
- $figure.classList.remove('loading')
- $figure.dataset.params = params
- $figure.querySelector('figcaption').innerHTML = params
- })
- }
- const observer = new MutationObserver(mutations => {
- for (let mutation of mutations) {
- for (let node of mutation.addedNodes) {
- // 노드가 이미지 태그가 아니라면 무시하기
- if (!(node instanceof HTMLImageElement)) {
- continue
- }
- node.addEventListener('load', onLoad)
- }
- }
- })
- observer.observe(
- document,
- {
- attributes: true,
- childList: true,
- subtree: true
- }
- )