NutAID Consolidated Script

Nut's All Image Downloader.

  1. // ==UserScript==
  2. // @name NutAID Consolidated Script
  3. // @match *://*/*
  4. // @version 1.2.0-indev4
  5. // @author nutzlos
  6. // @description Nut's All Image Downloader.
  7. // @run-at document-start
  8. // @inject-into content
  9. // @sandbox DOM
  10. // @grant GM_getValue
  11. // @grant GM_setValue
  12. // @grant GM_xmlhttpRequest
  13. // @connect *
  14.  
  15. // @namespace https://greasyfork.org/users/1455562
  16. // ==/UserScript==
  17.  
  18.  
  19. (function (){
  20. const LOGGING = "console"; //possible values: false or "", "log" or true, "console"
  21. let OPTIONS = {
  22. trackingProtection: GM_getValue('trackingProtection', true),
  23. arbitraryFillStyle: GM_getValue('arbitraryFillStyle', false),
  24. allowText: GM_getValue('allowText', false),
  25. mergedDownloads: GM_getValue('mergedDownloads', false),
  26. binbMerging: GM_getValue('binbMerging', true),
  27. modifyImgSrcLoading: GM_getValue('modifyImgSrcLoading', false),
  28. keys: {
  29. toContext: 'xyyxyxyyxxxy',
  30. toPageTop: 'asaasssassaas'
  31. }
  32. }
  33.  
  34. let keySeed = GM_getValue('communicationKey', {
  35. lastUsed: -Infinity,
  36. value: 0
  37. })
  38. //generate new one if key has been unused for more than 2 hours
  39. if (new Date() - keySeed.lastUsed > 7.2e6) {
  40. keySeed.value = (Math.random() * 1e16) & (0xffffffff)
  41. }
  42. keySeed.lastUsed = (new Date()).valueOf()
  43. GM_setValue('communicationKey', keySeed)
  44.  
  45. const generateKey = ((seed) => {
  46. let state = seed
  47. const xorshift = () => {
  48. state ^= state << 13
  49. state ^= state >> 17
  50. state ^= state << 5
  51. return state
  52. }
  53. let cipher = 'abcdefghijklmnopQRSTUVWxyzABCDEFGHIJKLMNOPqrstuvwXYZ'
  54. return (length, maxLength) => {
  55. if (maxLength && maxLength != length) {
  56. length = Math.abs(xorshift() % (maxLength - length)) + length
  57. }
  58. let key = ''
  59. for (let i = length; i > 0; --i) {
  60. key += cipher.charAt(Math.abs(xorshift() % cipher.length))
  61. }
  62. return key
  63. }
  64. })(keySeed.value)
  65.  
  66. OPTIONS.keys.toContext = generateKey(30)
  67. OPTIONS.keys.toPageTop = generateKey(30)
  68.  
  69.  
  70. let pageScript = function (OPTIONS){
  71. let targetWindow = this
  72. //cross origin iframes will not be able to dispatch events to the top level window.
  73. //even the content script cannot work around that without being detectable.
  74. //therefore, we need to add nested menus
  75. let windowtop = targetWindow //since this is run in an iframe for added isolation, the target window will be the parent
  76. try {
  77. while (windowtop != window.top) {
  78. if ('dispatchEvent' in windowtop.parent) {
  79. windowtop = windowtop.parent
  80. } else {
  81. break
  82. }
  83. }
  84. } catch (e) {}
  85.  
  86. const logger = (function () {
  87. return (title, that, args) => {
  88. if (title.includes('toString')) return;
  89. let e = new CustomEvent(OPTIONS.keys.toPageTop, {
  90. detail: {
  91. action: 'log',
  92. title: title,
  93. that: that,
  94. args: args,
  95. context: targetWindow
  96. }
  97. })
  98. windowtop.dispatchEvent(e)
  99. }
  100. })()
  101.  
  102. if (targetWindow == windowtop) {
  103. function IndexTracker(){
  104. let values = []
  105. function getID(value) {
  106. if (!value) return null;
  107. let i = values.indexOf(value)
  108. if (i < 0) {
  109. values.push(value)
  110. i = values.indexOf(value)
  111. }
  112. return i
  113. }
  114. return getID
  115. }
  116. const capturedFramesIndex = new IndexTracker()
  117. capturedFramesIndex(this)
  118. targetWindow.addEventListener(OPTIONS.keys.toPageTop, (e) => {
  119. e.detail.context = capturedFramesIndex(e.detail.context)
  120. let ev = new CustomEvent(OPTIONS.keys.toContext, {
  121. detail: e.detail
  122. })
  123. dispatchEvent(ev)
  124. })
  125. }
  126.  
  127. const canvasToBlob = HTMLCanvasElement.prototype.toBlob
  128. const ctxDrawImage = CanvasRenderingContext2D.prototype.drawImage
  129. const canvasToDataURL = HTMLCanvasElement.prototype.toDataURL
  130. const createUrlFromBlob = URL.createObjectURL
  131. const imgSetSrc = Object.getOwnPropertyDescriptor(HTMLImageElement.prototype, 'src').set
  132.  
  133. let globalImageCounter = 0
  134. function captureNewImage(image = null, source = null, risky, scrambleParams) {
  135. //image = element the image was caught on, if applicable
  136. //source = the source object/element that was captured
  137. if (scrambleParams === undefined) {
  138. scrambleParams = dirtyFlag('get:params', image)
  139. }
  140. if (risky === undefined) {
  141. risky = dirtyFlag('get:risky', image) | dirtyFlag('get:risky', source)
  142. }
  143. let e = new CustomEvent(OPTIONS.keys.toPageTop, {
  144. detail: {
  145. action: 'captureImage',
  146. image: image,
  147. source: source,
  148. risky: risky,
  149. scrambleParams: scrambleParams,
  150. context: targetWindow
  151. }
  152. })
  153. windowtop.dispatchEvent(e)
  154. dirtyFlag('clear', image)
  155. }
  156.  
  157. function extensionFromMimeType(mime) {
  158. let extension
  159. switch (mime) {
  160. case 'image/png':
  161. extension = '.png'
  162. break;
  163. case 'image/webp':
  164. extension = '.webp'
  165. break;
  166. case 'image/gif':
  167. extension = '.gif'
  168. break;
  169. case 'image/avif':
  170. extension = '.avif'
  171. break;
  172. case 'image/jxl':
  173. extension = '.jxl'
  174. break;
  175. case 'image/svg+xml':
  176. extension = '.svg'
  177. break;
  178. default:
  179. extension = '.jpeg'
  180. break
  181. }
  182. return extension
  183. }
  184. function GM_xmlhttpRequest_asyncWrapper (urlToFetch) {
  185. return new Promise((resolve, reject) => {
  186. let url = new URL(urlToFetch, location.href)
  187. let sameOrigin = urlToFetch.startsWith(location.origin)
  188. GM_xmlhttpRequest({
  189. url: url.href,
  190. responseType: 'blob',
  191. anonymous: false,
  192. headers: {
  193. 'Referer': location.origin + '/',
  194. 'Sec-Fetch-Dest': 'image',
  195. 'Sec-Fetch-Mode': 'no-cors',
  196. 'Sec-Fetch-Site': sameOrigin ? 'same-origin' : 'cross-site',
  197. 'Pragma': 'no-cache',
  198. 'Cache-Control': 'no-cache'
  199. },
  200. onload: resolve,
  201. onerror: reject
  202. })
  203. })
  204. }
  205. async function fetchImg(url) {
  206. if (url.startsWith('http')) {
  207. let r = await GM_xmlhttpRequest_asyncWrapper(url)
  208. let mime = r.responseHeaders.match(/content-type: (.*)/i)
  209. return {
  210. data: await r.response,
  211. contentType: mime && mime[1]
  212. }
  213. } else {
  214. let r = await fetch(url, {cache: 'force-cache'})
  215. return {
  216. data: await r.blob(),
  217. contentType: r.headers.get('content-type')
  218. }
  219. }
  220. }
  221.  
  222. async function dlAllImgs() {
  223. let promises = []
  224. let filenameCounter = 0
  225. let fileCompletedCounter = 0
  226. dledImgsCounterElement.innerText = '(0 imgs)'
  227. async function getImg(x, i) {
  228. const url = x.getAttribute('data-original') || x.getAttribute('data-src') || x.getAttribute('content')
  229. let img
  230. try {
  231. img = await fetchImg(url)
  232. } catch (e) {
  233. img = await fetchImg(x.src)
  234. }
  235. let extension = extensionFromMimeType(img.contentType)
  236. let filename = String(i).padStart(4, '0')
  237. dledImgsCounterElement.innerText = `(${++fileCompletedCounter} imgs)`
  238. return {
  239. name: filename + extension,
  240. data: await img.data.arrayBuffer()
  241. }
  242. }
  243. for (let x of document.getElementsByTagName('img')) {
  244. promises.push(getImg(x, ++filenameCounter))
  245. }
  246. Promise.allSettled(promises).then((p) => {
  247. let files = []
  248. p.map(x => (x.status == 'fulfilled') && files.push(x.value))
  249. let zipData = SimpleZip.GenerateZipFrom(files)
  250. let blob = new Blob([zipData], {type: "octet/stream"})
  251. var url = createUrlFromBlob(blob);
  252. createDownload(url, (+new Date())+'.zip')
  253. dledImgsCounterElement.innerText = ''
  254. })
  255. }
  256.  
  257. let dirtyFlagPerCanvas = new Map()
  258. /* Layout of the objects for the above: {
  259. * dirty: bool,
  260. * risky: bool,
  261. * timer: int,
  262. * lastSource: drawImage source, //used for scrambled images
  263. * drawImageSequence: [coords] //scrambled images
  264. }*/
  265. function dirtyFlag(op, canvas, img_source=null, drawImageParams) {
  266. if (op.startsWith('set')) {
  267. let o = {
  268. dirty: true,
  269. risky: false,
  270. timer: null,
  271. lastSource: null,
  272. drawImageSequence: []
  273. }
  274. if (dirtyFlagPerCanvas.has(canvas)) {
  275. o = dirtyFlagPerCanvas.get(canvas)
  276. o.dirty = true
  277. }
  278. if (img_source) {
  279. o.lastSource = img_source
  280. }
  281. if (op.includes('+timer')) {
  282. if (o.timer) {
  283. clearTimeout(o.timer)
  284. }
  285. o.timer = setTimeout((y,z)=>captureNewImage(y, z), 500, canvas, img_source)
  286. }
  287. if (op.includes('+risky')) {
  288. o.risky = true
  289. }
  290. if (op.includes('+params')) {
  291. if (drawImageParams) o.drawImageSequence.push(drawImageParams)
  292. }
  293. dirtyFlagPerCanvas.set(canvas, o)
  294. }
  295. if (op == 'clear') {
  296. if (dirtyFlagPerCanvas.has(canvas)) {
  297. o = dirtyFlagPerCanvas.get(canvas)
  298. if (o.timer) {
  299. clearTimeout(o.timer)
  300. }
  301. dirtyFlagPerCanvas.delete(canvas)
  302. }
  303. }
  304. if (op.startsWith('get')) {
  305. if (dirtyFlagPerCanvas.has(canvas)) {
  306. o = dirtyFlagPerCanvas.get(canvas)
  307. if (op == 'get:risky') {
  308. return o.risky
  309. }
  310. if (op == 'get:source') {
  311. return o.lastSource
  312. }
  313. if (op == 'get:params') {
  314. return o.drawImageSequence
  315. }
  316. return o.dirty
  317. } else {
  318. return false
  319. }
  320. }
  321. }
  322.  
  323.  
  324.  
  325. const urlToBlobMapping = {}
  326.  
  327.  
  328. function setupIntercept(window){
  329. const funcsToConceil = new Map()
  330. const originalFuncs = new Map()
  331. const orig = (f) => originalFuncs.get(f)
  332.  
  333. function createInterceptorFunction(originalFunction, newFunction, baseObj) {
  334. let originalProps = Object.getOwnPropertyDescriptors(originalFunction)
  335. let loggingTag = baseObj[Symbol.toStringTag]+'.'
  336. loggingTag += originalProps.name.value.includes(' ') ? `[${originalProps.name.value}]` : originalProps.name.value
  337. let interceptor = {
  338. fuckShit(){
  339. logger(loggingTag, this, arguments)
  340. return newFunction.apply(this, arguments)
  341. }
  342. }.fuckShit
  343. Object.defineProperties(interceptor, originalProps)
  344. funcsToConceil.set(interceptor, originalFunction)
  345. originalFuncs.set(newFunction, originalFunction)
  346. return interceptor
  347. }
  348.  
  349. function interceptFunction(obj, prop, fun) {
  350. const old = obj[prop]
  351. let ifunc = createInterceptorFunction(old, fun, obj)
  352. obj[prop] = ifunc
  353. }
  354. function interceptProperty(obj, prop, getOrSet, fun) {
  355. const old = Object.getOwnPropertyDescriptor(obj, prop)
  356. if (typeof old[getOrSet] != 'function') {
  357. console.warn('Risky interceptor for ', fun)
  358. debugger
  359. }
  360. let ifunc = createInterceptorFunction(old[getOrSet], fun, obj)
  361. let x = {}
  362. x[getOrSet] = ifunc
  363. Object.defineProperty(obj, prop, x)
  364. }
  365.  
  366. interceptFunction(window.Function.prototype, 'toString', function toString(){
  367. return orig(toString).apply(funcsToConceil.get(this)||this, arguments)
  368. })
  369.  
  370. //image interception
  371. interceptFunction(window.CanvasRenderingContext2D.prototype, 'drawImage', function drawImage(...args) {
  372. //do what needs to be done
  373. let img_source = args[0], img
  374.  
  375. let oldsrc = dirtyFlag('get:source', this.canvas)
  376. if (oldsrc && oldsrc != img_source) {
  377. //source changes are sus, rip to be on the safe side
  378. captureNewImage(this.canvas, oldsrc)
  379. dirtyFlag('set+risky', this.canvas)
  380. }
  381.  
  382. // if (img_source.toString() == "[object HTMLImageElement]" && img_source.naturalHeight == 0) debugger
  383. if (args.length == 3 || (
  384. args.length == 5 &&
  385. args[1] == 0 &&
  386. args[2] == 0 &&
  387. args[3] == img_source.width &&
  388. args[4] == img_source.height
  389. )
  390. ) {
  391. //no cropping of the source image, or it covers the whole canvas
  392. if (dirtyFlag('get', this.canvas)) {
  393. captureNewImage(this.canvas, dirtyFlag('get:source', this.canvas))
  394. }
  395. //dirtyFlag('clear', this.canvas) //done by captureNewImage, in theory that should be enough
  396. let source = img_source
  397.  
  398. //set scrambling param just in case only part of the image is scrambled
  399. //make the params compatible with the full length drawImage arguments
  400. let fullLengthArgs = [
  401. 0, 0, //source origin
  402. img_source.width, img_source.height,//source dimensions
  403. 0, 0, //target origin
  404. img_source.width, img_source.height //target dimensions
  405. ]
  406. dirtyFlag('set+params', this.canvas, img_source, fullLengthArgs)
  407.  
  408. captureNewImage(this.canvas, img_source)
  409. } else if (args.length == 9) {
  410. //need to canvas rip because the image is likely to be scrambled
  411. dirtyFlag('set+timer+params', this.canvas, img_source, args.slice(1))
  412. }
  413. //call the proper function
  414. return ctxDrawImage.apply(this, args)
  415. })
  416.  
  417. function ignoreSource(source) {
  418. let e = new CustomEvent(OPTIONS.keys.toPageTop, {
  419. detail: {
  420. action: 'ignoreSource',
  421. source: source,
  422. context: window
  423. }
  424. })
  425. windowtop.dispatchEvent(e)
  426. }
  427. interceptFunction(window.HTMLCanvasElement.prototype, 'toBlob', function toBlob() {
  428. if (dirtyFlag('get', this)) {
  429. let src = dirtyFlag('get:source', this)
  430. //If no image made its way to the canvas, then there's no need to capture it
  431. if (src) captureNewImage(this, src)
  432. }
  433. return canvasToBlob.call(this, (b)=>{
  434. ignoreSource(b)
  435. arguments[0](b)
  436. })
  437. })
  438. interceptFunction(window.HTMLCanvasElement.prototype, 'toDataURL', function toDataURL() {
  439. if (dirtyFlag('get', this)) {
  440. let src = dirtyFlag('get:source', this)
  441. //If no image made its way to the canvas, then there's no need to capture it
  442. if (src) captureNewImage(this, src)
  443. }
  444. let uri = canvasToDataURL.apply(this, arguments)
  445. ignoreSource(uri)
  446. return uri
  447. })
  448. interceptFunction(window.CanvasRenderingContext2D.prototype, 'putImageData', function putImageData() {
  449. dirtyFlag('set+risky+timer', this.canvas)
  450. const ret = orig(putImageData).apply(this, arguments)
  451. if (arguments[0].width == this.canvas.width && arguments[0].height == this.canvas.height) {
  452. captureNewImage(this.canvas, arguments[0])
  453. }
  454. return ret
  455. })
  456. interceptFunction(window.CanvasRenderingContext2D.prototype, 'createPattern', function createPattern() {
  457. //capture the image that's passed in but don't link it to this canvas as technically
  458. //nothing happened just yet and we don't want to reset the dirty flag just yet
  459. captureNewImage('canvaspattern', arguments[0])
  460. let pattern = orig(createPattern).apply(this, arguments)
  461. ignoreSource(pattern)
  462. return pattern
  463. })
  464.  
  465. interceptFunction(window.URL, 'createObjectURL', function createObjectURL() {
  466. let url = createUrlFromBlob(...arguments)
  467. let blob = arguments[0]
  468. urlToBlobMapping[url] = blob
  469. let e = new CustomEvent(OPTIONS.keys.toPageTop, {
  470. detail: {
  471. action: 'urlToBlob',
  472. url: url,
  473. blob: blob,
  474. context: window
  475. }
  476. })
  477. windowtop.dispatchEvent(e)
  478. if (blob instanceof Blob && blob.type.startsWith('image')) {
  479. captureNewImage('createObjectURL', blob)
  480. } else {
  481. // blob.arrayBuffer().then(a => {
  482. // let u = new Uint8Array(a)
  483. // if (
  484. // (u[0] === 0xFF && u[1] === 0xD8 && u[2] === 0xFF) || //JPG
  485. // (u[1] === 0x50 && u[2] === 0x4E && u[3] === 0x47) || //PNG
  486. // (u[8] === 0x57 && u[9] === 0x45 && u[10] === 0x42) || //Web(P)
  487. // (u[0] === 0x47 && u[1] === 0x49 && u[2] === 0x46) //GIF
  488. // ) {
  489. // captureNewImage('createObjectURL', blob)
  490. // }
  491. // })
  492.  
  493. //mime sniffing is clearly insufficient, there's too many image formats to hardcode, and there could be more in the future
  494. let i = new Image()
  495. i.onload = ()=> captureNewImage('createObjectURL', blob)
  496. imgSetSrc.call(i, url)
  497. }
  498. return url
  499. })
  500. // interceptFunction(window.URL, 'revokeObjectURL', function revokeObjectURL() {
  501. // return undefined
  502. // })
  503. interceptProperty(window.HTMLImageElement.prototype, 'src', 'set', function setSrc() {
  504. const url = arguments[0]
  505. if (url && url.startsWith('blob:') || url.startsWith('data:')) {
  506. captureNewImage(this, url)
  507. orig(setSrc).apply(this, arguments)
  508. } else if (OPTIONS.modifyImgSrcLoading && !this.crossOrigin) {
  509. GM_xmlhttpRequest_asyncWrapper(url).then((resp) => {
  510. captureNewImage(this, resp.response)
  511. let u = URL.createObjectURL(resp.response)
  512. orig(setSrc).call(this, u)
  513. }).catch((e) => {
  514. orig(setSrc).apply(this, arguments)
  515. })
  516. } else {
  517. captureNewImage(this, url)
  518. orig(setSrc).apply(this, arguments)
  519. }
  520. })
  521.  
  522.  
  523. //block APIs useful for fingerprinting / tracking
  524. interceptFunction(window.CanvasRenderingContext2D.prototype, 'clearRect', function clearRect(){
  525. if (arguments[2] != this.canvas.width && arguments[3] != this.canvas.height) {
  526. if (!OPTIONS.trackingProtection) {
  527. dirtyFlag('set+risky', this.canvas)
  528. return orig(clearRect).apply(this, arguments)
  529. } else {
  530. return
  531. }
  532. }
  533. if (dirtyFlag('get', this.canvas)) {
  534. let src = dirtyFlag('get:source', this.canvas)
  535. if (src) captureNewImage(this.canvas, src)
  536. }
  537. return orig(clearRect).apply(this, arguments)
  538. })
  539. //setting canvas width/height can also clear the canvas
  540. interceptProperty(window.HTMLCanvasElement.prototype, 'width', 'set', function setWidth(){
  541. if (dirtyFlag('get', this)) {
  542. let src = dirtyFlag('get:source', this)
  543. //If no image made its way to the canvas, then there's no need to capture it
  544. if (src) captureNewImage(this, src)
  545. }
  546. return orig(setWidth).apply(this, arguments)
  547. })
  548. interceptProperty(window.HTMLCanvasElement.prototype, 'width', 'set', function setHeight(){
  549. if (dirtyFlag('get', this)) {
  550. let src = dirtyFlag('get:source', this)
  551. //If no image made its way to the canvas, then there's no need to capture it
  552. if (src) captureNewImage(this, src)
  553. }
  554. return orig(setHeight).apply(this, arguments)
  555. })
  556. interceptFunction(window.CanvasRenderingContext2D.prototype, 'fillRect', function fillRect(){
  557. if (arguments[2] != this.canvas.width && arguments[3] != this.canvas.height) {
  558. if (!OPTIONS.trackingProtection) {
  559. dirtyFlag('set+risky', this.canvas)
  560. return orig(fillRect).apply(this, arguments)
  561. } else {
  562. return
  563. }
  564. }
  565. if (dirtyFlag('get', this.canvas)) {
  566. let src = dirtyFlag('get:source', this.canvas)
  567. //If no image made its way to the canvas, then there's no need to capture it
  568. if (src) captureNewImage(this.canvas, src)
  569. }
  570. if (OPTIONS.arbitraryFillStyle)
  571. dirtyFlag('set+risky', this.canvas);
  572. return orig(fillRect).apply(this, arguments)
  573. })
  574. interceptFunction(window.CanvasRenderingContext2D.prototype, 'strokeRect', function strokeRect() {
  575. if (!OPTIONS.trackingProtection) {
  576. dirtyFlag('set+risky', this.canvas)
  577. return orig(strokeRect).apply(this, arguments)
  578. } else {
  579. return
  580. }
  581. })
  582. interceptFunction(window.CanvasRenderingContext2D.prototype, 'fill', function fill() {
  583. if (!OPTIONS.trackingProtection) {
  584. dirtyFlag('set+risky', this.canvas)
  585. return orig(fill).apply(this, arguments)
  586. } else {
  587. return
  588. }
  589. })
  590. interceptFunction(window.CanvasRenderingContext2D.prototype, 'stroke', function stroke() {
  591. if (!OPTIONS.trackingProtection) {
  592. dirtyFlag('set+risky', this.canvas)
  593. return orig(stroke).apply(this, arguments)
  594. } else {
  595. return
  596. }
  597. })
  598. //should text be blocked too?
  599. //it can be useful despite tracking possibility
  600. //if we block transparency, that shouldn't pose too much of a risk
  601. interceptProperty(window.CanvasRenderingContext2D.prototype, 'globalAlpha', 'set', function setAlpha(){
  602. if (OPTIONS.trackingProtection) {
  603. return orig(setAlpha).call(this, Math.round(arguments[0]))
  604. } else {
  605. return orig(setAlpha).call(this, arguments[0])
  606. }
  607. })
  608. interceptProperty(window.CanvasRenderingContext2D.prototype, 'fillStyle', 'set', function setStyle(){
  609. if (OPTIONS.trackingProtection && !OPTIONS.arbitraryFillStyle) {
  610. return orig(setStyle).call(this, '#f60')
  611. } else {
  612. return orig(setStyle).apply(this, arguments)
  613. }
  614. })
  615. interceptFunction(window.CanvasRenderingContext2D.prototype, 'fillText', function fillText() {
  616. if (OPTIONS.trackingProtection && !OPTIONS.allowText) {
  617. return
  618. } else {
  619. dirtyFlag('set+risky', this.canvas)
  620. return orig(fillText).apply(this, arguments)
  621. }
  622. })
  623. interceptFunction(window.CanvasRenderingContext2D.prototype, 'strokeText', function strokeText() {
  624. if (OPTIONS.trackingProtection && !OPTIONS.allowText) {
  625. return
  626. } else {
  627. dirtyFlag('set+risky', this.canvas)
  628. return orig(strokeText).apply(this, arguments)
  629. }
  630. })
  631.  
  632.  
  633. // //don't let sites get away by sourcing their functions/prototypes from an iframe
  634. // interceptProperty(window.HTMLIFrameElement.prototype, 'contentWindow', 'get', function getIFrame(){
  635. // let iframeWindow = orig(getIFrame).call(this)
  636. // try {
  637. // setupIntercept(iframeWindow)
  638. // } catch (all) {}
  639. // return iframeWindow
  640. // })
  641.  
  642. // ^ should be handled by userscript manager
  643. }
  644. setupIntercept(targetWindow)
  645.  
  646. console.log('cr page script loaded')
  647. }
  648. //insert page script into page
  649. let injectionScript = document.createElement('script')
  650. // ifr.src = 'about:blank'
  651. // let s = document.createElement('script')
  652. let injectionCode = `
  653. (${pageScript.toString()})(${JSON.stringify(OPTIONS)});
  654. document.currentScript.remove()
  655. `;
  656. let injectionBlob = new Blob([injectionCode], {type:'application/javascript'});
  657. let injectionUrl = URL.createObjectURL(injectionBlob);
  658. injectionScript.setAttribute('src', injectionUrl);
  659. (document.body || document.documentElement || document).insertAdjacentElement('afterbegin', injectionScript);
  660.  
  661.  
  662. //cross origin iframes will not be able to dispatch events to the top level window.
  663. //even the content script cannot work around that without being detectable.
  664. //therefore, we need to add nested menus
  665. let windowtop = window
  666. try {
  667. while (windowtop != window.top) {
  668. if ('dispatchEvent' in windowtop.parent) {
  669. windowtop = windowtop.parent
  670. } else {
  671. break
  672. }
  673. }
  674. } catch (e) {}
  675.  
  676. //insert UI and Content script only once on the top level document
  677. if (window == windowtop) {
  678.  
  679. function IndexTracker(){
  680. let values = []
  681. function getID(value) {
  682. if (!value) return null
  683. let i = values.indexOf(value)
  684. if (i < 0) {
  685. values.push(value)
  686. i = values.indexOf(value)
  687. }
  688. return i
  689. }
  690. return getID
  691. }
  692.  
  693.  
  694. let NutZip
  695. (()=>{
  696. NutZip=function(){const p="byteLength";async function d(d){const h=d.crcLut;return async function(e,t){var{nameLength:e,data:a}=d.file,r=function(e){let[t,a,r,n,o,s,f,i]=h,c=-1,u=0;for(var l,g,w=new Uint32Array(e.buffer,0,e.buffer.byteLength>>>2),p=4294967294&w.length;u<p;)l=w[u++]^c,g=w[u++],c=i[255&l]^f[l>>>8&255]^s[l>>>16&255]^o[l>>>24]^n[255&g]^r[g>>>8&255]^a[g>>>16&255]^t[g>>>24];let d=4*u;for(;d<e.length;)c=c>>>8^t[255&c^e[d++]];return~c}(a=new Uint8Array(a)),n=t?(o=a,n=new CompressionStream("deflate-raw"),o=new Response(o).body.pipeThrough(n),await new Response(o).arrayBuffer()):a.buffer,o=new ArrayBuffer(30),s=new DataView(o),f=new ArrayBuffer(46),i=new DataView(f),c=(g=new Date).getFullYear(),u=g.getMonth()+1,l=g.getDate(),g=g.getHours()<<11|g.getMinutes()<<5|g.getSeconds()>>>1,c=(c<1980?0:2107<c?127:c-1980)<<9|u<<5|l;let[w,p]=(u=a=>[(e,t)=>a.setUint16(e,t,!0),(e,t)=>a.setUint32(e,t,!0)])(s);return p(0,67324752),t?w(4,2580):w(4,2570),w(6,2048),t?w(8,8):w(8,0),w(10,g),w(12,c),p(14,r),p(18,n.byteLength),p(22,a.byteLength),w(26,e),[w,p]=u(i),p(0,33639248),w(4,2623),t?w(6,2580):w(6,2570),w(8,2048),t?w(10,8):w(10,0),w(12,g),w(14,c),p(16,r),p(20,n.byteLength),p(24,a.byteLength),w(28,e),{data:n,localHeader:o,centralHeader:f}}(0,d.compress)}const h=function(){var e=Array.from({length:8},()=>new Uint32Array(256)),[a,t,r,n,o,s,f,i]=e;for(let e=0;e<=255;e++){let t=e;for(let e=0;e<8;e++)t=t>>>1^3988292384*(1&t);a[e]=t}for(let e=0;e<=255;e++)t[e]=a[e]>>>8^a[255&a[e]],r[e]=t[e]>>>8^a[255&t[e]],n[e]=r[e]>>>8^a[255&r[e]],o[e]=n[e]>>>8^a[255&n[e]],s[e]=o[e]>>>8^a[255&o[e]],f[e]=s[e]>>>8^a[255&s[e]],i[e]=f[e]>>>8^a[255&f[e]];return e}(),y=new TextEncoder;return async function(e,a=!1){const r=e.map(e=>y.encode(e.name).buffer);var t,n,g=d,w=e.map((e,t)=>({args:{file:{data:e=(e="string"==typeof(e=e.data)?y.encode(e):e).buffer&&"object"==typeof e.buffer?e.buffer:e,nameLength:r[t][p]},compress:a,crcLut:h},transfer:[e]})),o=[];let s=0,f=0;for(t of c=await new Promise(r=>{const t=w.length;let n=t,a=-1,o=[],s=[];var e=`${(e=g).toString()};onmessage=async e=>{var a=await ${e.name}(e.data.p.args);let s=[];const t=e=>{if("object"==typeof e){"ArrayBuffer"==e[Symbol.toStringTag]&&s.push(e);for(var a of Object.values(e))t(a)}};t(a),postMessage({r:a,i:e.data.i},s)};`,f=URL.createObjectURL(new Blob([e])),i=Math.min(t,navigator.hardwareConcurrency);const c=e=>{++a<t&&e.postMessage({i:a,p:w[a]},w[a].transfer)};var u=e=>{var t=e.data.i;if(o[t]=e.data.r,0==--n){for(var a of s)a.terminate();r(o)}else c(e.srcElement)};for(let e=0;e<i;++e){var l=new Worker(f);l.onmessage=u,s.push(l),c(l)}}))t.offset=s,o.push(t.localHeader),o.push(r[f]),o.push(t.data),s+=t.localHeader[p]+r[f++][p]+t.data[p];let i=0;f=0;for(n of c)new DataView(n.centralHeader).setUint32(42,n.offset,!0),o.push(n.centralHeader),o.push(r[f]),i+=n.centralHeader[p]+r[f++][p];var c=new ArrayBuffer(22),u=(l=new DataView(c)).setUint32.bind(l),l=l.setUint16.bind(l);return u(0,101010256,!0),l(8,e.length,!0),l(10,e.length,!0),u(12,i,!0),u(16,s,!0),o.push(c),new Blob(o,{type:"application/zip"})}}();
  697.  
  698. })()
  699.  
  700. let LOG = '#,action,"origin object id",params\n'
  701. const logger = (function () {
  702. if (!LOGGING) return ()=>undefined;
  703. var logCount = 0
  704. var origins = new IndexTracker()
  705. const objToID = (obj, frame) => {
  706. let frameID = frame
  707. let i = origins(obj)
  708. return `#${frameID}/${i}`
  709. }
  710. return (title, that, args, frame) => {
  711. if (!that) that = '';
  712. if (that.canvas) that = that.canvas;
  713. let x = objToID(that, frame)
  714. let argumentArray = Array.from(args).map(
  715. x => typeof x == 'object' ? objToID(x, frame) : x
  716. )
  717. LOG += [
  718. logCount++,
  719. title,
  720. x,
  721. `"${JSON.stringify(argumentArray).replaceAll('"', '""')}"`
  722. ].join(',') + "\n";
  723. if (LOGGING == 'console') console.debug(title, 'on', x, 'with args:', args);
  724. }
  725. })()
  726.  
  727.  
  728.  
  729. let dledImgsCounterElement
  730. let overlay = document.createElement('tbody')
  731. document.addEventListener("DOMContentLoaded", (event) => {
  732. // let divName
  733. // do {
  734. // divName = generateKey(3, 10)
  735. // } while ((document.getElementsByTagName(divName)).length)
  736. // const div = document.createElement(divName)
  737. const div = document.createElement('div')
  738. const shadow = div.attachShadow({mode: 'closed'})
  739. shadow.innerHTML = `
  740. <details id="_____cr" style="position: fixed; bottom: 0; left: 0; background-color: white; color: black; font-size: small; z-index: 99999999999999999;max-height:100%;max-width:100%">
  741. <div style="width: 300px; height: 300px; overflow: scroll">
  742. <div style="position:sticky;top:0;background:white;z-index:1">
  743. Bulk <button>download</button> all selected images <br>
  744. Selection: <button title="Select all found images.">All</button> <button title="Deselect all">None</button> <span title="Select all found images starting with the respective letter."><button>b</button> <button>c</button> <button>d</button> <button>e</button> <button>i</button> <button>p</button></span> <br>
  745. <details>
  746. <summary><small>Problems? Click here!</small></summary>
  747. <small style="padding-left: 1em; display: block;">
  748. <em> Changes to the below options will require reloading the page to take effect. </em>
  749. <details>
  750. <summary><input id="trackingProtection" type="checkbox"> Prevent insertion of tracking data </summary>
  751. <div style="padding-left: 1em; display: block;">
  752. This blocks several APIs often used to insert hidden tracking pixels or account-identifying watermarks. Of course, no protection measures can be 100% effective, and this is entirely useless if the site adds tracking data server-side. Also, this could potentially be detected by the website.
  753. <details>
  754. <summary><input id="arbitraryFillStyle" type="checkbox"> Allow arbitrary fillStyle</summary>
  755. Should for some reason images end up entirely orange, try ticking this checkbox. Note that websites might embed hidden tracking pixels this way.
  756. </details>
  757. <details>
  758. <summary><input id="allowText" type="checkbox"> Allow drawing text </summary>
  759. This poses a big risk of hidden watermark insertion but sometimes text drawn this way can include useful information.
  760. </details>
  761. <hr>
  762. </div>
  763. </details>
  764. <details>
  765. <summary><input id="mergedDownloads" type="checkbox"> Merge split pages (broken)</summary>
  766. Doesn't work properly at the moment, don't use this. If required, use the Firefox Add-On port of this userscript.
  767. </details>
  768. <details>
  769. <summary><input id="modifyImgSrcLoading" type="checkbox"> Modify &lt;img&gt; loading</summary>
  770. If images fail to load, cannot be captured or cannot be downloaded, try enabling this option. Intercepts most image loading and routes it through the UserScript manager to bypass CORS restrictions. This should be relatively safe, but could potentially be detected by the website.
  771. </details>
  772. <details>
  773. <summary><button>Download</button> all &lt;img&gt;s currently on the page <span id="dlcounter">(slow)</span></summary>
  774. Basically like the classic image downloading browser add-ons. Make sure to scroll through the entire page first to make sure all images have actually loaded. Note that this is completely unrelated to the captured image list and other functionality of this UserScript.
  775. </details>
  776. <details>
  777. <summary><button>Save</button> logs of intercepted functions</summary>
  778. For debugging purposes to investigate what a website might be doing.
  779. </details>
  780. </small>
  781. </details>
  782. <hr>
  783. </div>
  784. <table style="width:100%"></table>
  785. </div>
  786. <summary>Show</summary>
  787. </details>`
  788. let buttons = shadow.querySelectorAll('button')
  789. buttons[0].addEventListener('click', dlSelected)
  790. buttons[1].addEventListener('click', ()=>selectAll(true))
  791. buttons[2].addEventListener('click', ()=>selectAll(false))
  792. buttons[3].addEventListener('click', ()=>selectAllOf('b'))
  793. buttons[4].addEventListener('click', ()=>selectAllOf('c'))
  794. buttons[5].addEventListener('click', ()=>selectAllOf('d'))
  795. buttons[6].addEventListener('click', ()=>selectAllOf('e'))
  796. buttons[7].addEventListener('click', ()=>selectAllOf('i'))
  797. buttons[8].addEventListener('click', ()=>selectAllOf('p'))
  798. buttons[9].addEventListener('click', dlAllImgs)
  799. buttons[10].addEventListener('click', ()=> {
  800. createDownload('data:text/plain,'+encodeURIComponent(LOG), 'crlog'+(+new Date())+'.csv')
  801. })
  802. for (let x in OPTIONS) {
  803. if (typeof OPTIONS[x] != 'boolean') continue;
  804. let check = shadow.getElementById(x)
  805. if (!check) continue;
  806. check.checked = OPTIONS[x]
  807. check.addEventListener('change', function(){
  808. GM_setValue(this.id, this.checked)
  809. })
  810. }
  811. dledImgsCounterElement = shadow.querySelector('#dlcounter')
  812. shadow.querySelector('table').appendChild(overlay)
  813. div.style.display = 'block'
  814. div.style.width = '0'
  815. div.style.height = '0'
  816. document.documentElement.appendChild(div)
  817. });
  818.  
  819. const canvasToBlob = HTMLCanvasElement.prototype.toBlob
  820. const ctxDrawImage = CanvasRenderingContext2D.prototype.drawImage
  821. const canvasToDataURL = HTMLCanvasElement.prototype.toDataURL
  822. const createUrlFromBlob = URL.createObjectURL
  823. const imgSetSrc = Object.getOwnPropertyDescriptor(HTMLImageElement.prototype, 'src').set
  824. function ctob(canvas, ...args) {
  825. return new Promise(function(resolve) {
  826. canvasToBlob.apply(canvas, [resolve, ...args])
  827. })
  828. }
  829.  
  830. const previewImageSize = 50
  831.  
  832. const capturedImages = new Map()
  833. const ignoredSources = []
  834. let globalImageCounter = 0
  835. const captureNewImage = (function() {
  836. function copyImage(image) {
  837. if (typeof image == 'string') {
  838. return copyToImg(image)
  839. }
  840. switch (image.toString()) {
  841. case "[object HTMLImageElement]":
  842. return copyToImg(image.src)
  843. break
  844. case "[object Blob]":
  845. return copyToImg(createUrlFromBlob(image))
  846. break
  847. case "[object HTMLCanvasElement]":
  848. default:
  849. return copyToCanvas(image)
  850. }
  851. }
  852. function copyToImg(url) {
  853. let img = new Image()
  854. img.style.maxWidth = previewImageSize+'px'
  855. img.style.maxHeight = previewImageSize+'px'
  856. imgSetSrc.call(img, url)
  857. return img
  858. }
  859. function copyToCanvas(image, scramble) {
  860. if (!(scramble?.compare?.size > 1))
  861. scramble = {};
  862. let c = document.createElement('canvas')
  863. c.width = scramble.w || image.naturalWidth || image.width
  864. c.height = scramble.h || image.naturalHeight || image.height
  865. c.style.maxWidth = previewImageSize+'px'
  866. c.style.maxHeight = previewImageSize+'px'
  867. let ctx = c.getContext('2d')
  868. if (scramble.compare && scramble.compare.size) {
  869. ctxDrawImage.call(ctx, image, scramble.x, scramble.y, c.width, c.height, 0, 0, c.width, c.height)
  870. } else {
  871. ctxDrawImage.call(ctx, image, 0, 0)
  872. }
  873. return c
  874. }
  875. function processScrramblingParams(params, thingToSave) {
  876. if (!params) params = [];
  877. const tTS_w = thingToSave.naturalWidth || thingToSave.width
  878. const tTS_h = thingToSave.naturalHeight || thingToSave.height
  879. let bounds = [Infinity, Infinity, -Infinity, -Infinity]
  880. for (let x of params) {
  881. bounds[0] = Math.min(bounds[0], x[4])
  882. bounds[1] = Math.min(bounds[1], x[5])
  883. bounds[2] = Math.max(bounds[2], x[4]+x[6])
  884. bounds[3] = Math.max(bounds[3], x[5]+x[7])
  885. }
  886. bounds[0] = Math.max(bounds[0], 0)
  887. bounds[1] = Math.max(bounds[1], 0)
  888. bounds[2] = Math.min(bounds[2], tTS_w)
  889. bounds[3] = Math.min(bounds[3], tTS_h)
  890. return {
  891. w: bounds[2] - bounds[0],
  892. h: bounds[3] - bounds[1],
  893. x: bounds[0],
  894. y: bounds[1],
  895. compare: new Set((params || []).map(x => x.slice(0, 4).join()))
  896. }
  897. }
  898. function mergeImages(...images) {
  899. let yOffset = 0
  900. if (OPTIONS.binbMerging) {
  901. yOffset = -4
  902. }
  903. let c = document.createElement('canvas')
  904. c.width = Math.max(...images.map(x => x.naturalWidth || x.width))
  905. c.height = images.reduce((a,x) => a + (x.naturalHeight || x.height), 0) + (images.length -1)*yOffset
  906. c.style.maxWidth = previewImageSize+'px'
  907. c.style.maxHeight = previewImageSize+'px'
  908. let ctx = c.getContext('2d')
  909. let y = 0
  910. for (let i of images) {
  911. let x = Math.floor((c.width - (i.naturalWidth || i.width))*0.5)
  912. ctxDrawImage.call(ctx, i, x, y)
  913. y += (i.naturalHeight || i.height) + yOffset
  914. }
  915. return c
  916. }
  917. function addPageToPile(obj) {
  918. //image = element the image was caught on, if applicable
  919. //source = the source object/element that was captured
  920. let {
  921. image,
  922. source,
  923. risky,
  924. scrambleParams,
  925. context
  926. } = obj
  927.  
  928. let isImageData = false, isScrambled = Boolean(scrambleParams && scrambleParams.length > 1), isFiltered = false
  929. globalImageCounter++
  930. if (!source && image.toString() == "[object HTMLImageElement]") {
  931. source = image.src
  932. }
  933. if (source.toString() == "[object HTMLImageElement]") {
  934. source = source.src
  935. }
  936. if (source.toString() == "[object HTMLCanvasElement]") {
  937. //carry over flags
  938. risky |= dirtyFlag('get:risky', source)
  939. }
  940. if (image.toString() == "[object HTMLCanvasElement]") {
  941. if (image.filter && image.filter != 'none') {
  942. isFiltered = true
  943. }
  944. }
  945. if (typeof source == 'string' && source.startsWith('blob:')) {
  946. source = urlToBlobMapping[source]
  947. }
  948. //no idea how to effectively dedupe ImageData
  949. if (source.toString() == "[object ImageData]") {
  950. //let's not keep that in memory too when we already never delete blobs
  951. source = 'imagedata-' + globalImageCounter
  952. isImageData = true
  953. }
  954. //there's probably more possible source types that I forgot, who cares
  955. if (ignoredSources.includes(source)) return false;
  956.  
  957. const thingToSave = (isScrambled || isImageData || isFiltered) ? image : source
  958. let scramble = processScrramblingParams(scrambleParams, thingToSave)
  959.  
  960. let existing = capturedImages.get(source)
  961. if (!existing) {
  962. //the template for a captured image entry
  963. let obj = {
  964. isScrambled: isScrambled,
  965. individual: [{
  966. savedImage: null,
  967. scrambleParams: scramble.compare,
  968. caughtOn: [image],
  969. isRisky: !!risky
  970. }],
  971. combined: {}
  972. }
  973. let i = obj.individual[0]
  974. if (isScrambled) {
  975. let c = copyToCanvas(thingToSave, scramble)
  976. i.savedImage = c
  977. } else {
  978. i.savedImage = copyImage(thingToSave)
  979. }
  980. capturedImages.set(source, obj)
  981. obj.combined = i
  982. return true
  983. } else {
  984. if (isScrambled) {
  985.  
  986. //compare if the scrambleParams are the same
  987. let exIdx
  988. if (
  989. existing.isScrambled &&
  990. (exIdx = existing.individual.findIndex(x => x.scrambleParams.isSubsetOf(scramble.compare))) >= 0
  991. ) {
  992. //same params were captured once already
  993. let exI = existing.individual[exIdx].savedImage
  994. if (exI.width >= scramble.w && exI.height >= scramble.h) {
  995. return false
  996. } else {
  997. //previous capture is likely to be incomplete, remove it and capture anew
  998. existing.individual.splice(exIdx, 1)
  999. }
  1000. }
  1001.  
  1002. let c = copyToCanvas(thingToSave, scramble)
  1003.  
  1004. if (existing.isScrambled) {
  1005. if (OPTIONS.mergedDownloads) {
  1006. let merged = mergeImages(existing.combined.savedImage, c)
  1007. let combi = {
  1008. savedImage: merged,
  1009. scrambleParams: '',
  1010. caughtOn: existing.combined.caughtOn.slice(),
  1011. isRisky: !!risky || existing.combined.risky
  1012. }
  1013. if (!combi.caughtOn.includes(image)) combi.caughtOn.push(image);
  1014. existing.combined = combi
  1015. }
  1016. existing.individual.push({
  1017. savedImage: c,
  1018. scrambleParams: scramble.compare,
  1019. caughtOn: [image],
  1020. isRisky: !!risky
  1021. })
  1022. } else {
  1023. //the still scrambled image was saved. discard it in favor of the now uncrambled addition
  1024. let obj = existing.combined //for whole images like the still scrambled page this should be the same object as individual[0]
  1025. obj.savedImage = copyImage(c)
  1026. obj.scrambleParams = scramble.compare.union(obj.scrambleParams)
  1027. if (!obj.caughtOn.includes(image)) obj.caughtOn.push(image);
  1028. obj.isRisky = !!risky || obj.isRisky
  1029. existing.isScrambled = true
  1030. }
  1031. return true
  1032. } else {
  1033. //probably the same thing, already exists
  1034. if (!existing.combined.caughtOn.includes(image))
  1035. existing.combined.caughtOn.push(image);
  1036. if (scramble.compare.size == 1)
  1037. existing.combined.scrambleParams = scramble.compare.union(existing.combined.scrambleParams);
  1038. return true
  1039. }
  1040. }
  1041. }
  1042. return function(obj) {
  1043. // console.log('captured Image:', obj)
  1044. addPageToPile(obj) && updateOverlay()
  1045. }
  1046. })()
  1047.  
  1048. function updateOverlay() {
  1049. let sourcedFrom = {
  1050. i: [], //normal urls caught on img
  1051. u: [], //blob urls caught on img
  1052. c: [], //drawImage interception on canvases
  1053. e: [], //scrambled images first captured as normal img (E like scrambled Eggs)
  1054. d: [], //ImageData interception on canvases
  1055. p: [], //createPattern interception
  1056. b: [] //createObjectURL interception
  1057. }
  1058. for (let x of capturedImages.entries()) {
  1059. if (typeof x[0] == 'string') {
  1060. if (x[0].startsWith('imagedata')) {
  1061. sourcedFrom.d.push(x)
  1062. } else {
  1063. if (x[1].isScrambled) {
  1064. sourcedFrom.e.push(x)
  1065. } else {
  1066. sourcedFrom.i.push(x)
  1067. }
  1068. }
  1069. } else {
  1070. if (typeof x[1].combined.caughtOn[0] == 'string') {
  1071. if (x[1].combined.caughtOn[0] == 'canvaspattern')
  1072. sourcedFrom.p.push(x);
  1073. else sourcedFrom.b.push(x)
  1074. } else {
  1075. if (x[1].combined.caughtOn[0] instanceof HTMLCanvasElement) {
  1076. sourcedFrom.c.push(x)
  1077. } else {
  1078. sourcedFrom.u.push(x) //should in theory remain empty as any such image should have gone thorugh createObjectURL prior
  1079. }
  1080. }
  1081. }
  1082. }
  1083. let docHTML = document.documentElement.innerHTML
  1084. let b = sourcedFrom['b'].map(x=>({
  1085. /* Looks up the corresponding blob URL, and finds it in the page HTML */
  1086. i: docHTML.indexOf( Object.keys(urlToBlobMapping)[Object.values(urlToBlobMapping).indexOf(x[0])] ),
  1087. x: x
  1088. }))
  1089. b.sort((a,b)=> a.i > b.i )
  1090. sourcedFrom['b'] = b.map(x=>x.x)
  1091. let allCanvases = Array.from(document.getElementsByTagName('canvas'))
  1092. let allImgs = Array.from(document.getElementsByTagName('img'))
  1093. overlay.innerHTML = ''
  1094. for (let cat in sourcedFrom) {
  1095. let offscreenCounter = 1
  1096. for (let x of sourcedFrom[cat]) {
  1097. let name = cat
  1098. let origin = x[1].combined.caughtOn.find(x=>(x instanceof HTMLImageElement || x instanceof HTMLCanvasElement) && x.parentElement != null)
  1099. let allOfThem = origin && origin instanceof HTMLImageElement ? allImgs : allCanvases
  1100. let n = 0
  1101. if (origin && (n = allOfThem.findIndex(node => node.isSameNode(origin)) + 1) && allOfThem.length >= sourcedFrom[cat].length) {
  1102. name += String(n).padStart(4, '0')
  1103. } else {
  1104. name += '_' + String(offscreenCounter++).padStart(4, '0')
  1105. }
  1106. //TODO do something with the individual vs combined images
  1107. let y;
  1108. if (OPTIONS.mergedDownloads) {
  1109. y = [x[1].combined]
  1110. } else {
  1111. y = x[1].individual
  1112. }
  1113. for (let i = 0; i < y.length; i++) {
  1114. let name2 = name, fileInfo = ''
  1115. if (y.length > 1) {
  1116. name2 += '-' + String(i+1).padStart(2, '0')
  1117. } else {
  1118. if (x[0] instanceof Blob && x[0].type) {
  1119. fileInfo = extensionFromMimeType(x[0].type)
  1120. } else if (typeof x[0] == 'string' && x[0].startsWith('imagedata')) {
  1121. fileInfo = '.png'
  1122. }
  1123. }
  1124. let z = y[i].savedImage
  1125. let riskBg = y[i].isRisky ? 'background: #ffc;' : ''
  1126. overlay.insertAdjacentHTML('beforeend', `<tr style="height: ${previewImageSize+5}px; ${riskBg}">
  1127. <td><input type="checkbox"></td>
  1128. <td style="max-width: ${previewImageSize}px; max-height: ${previewImageSize}px; position: relative"></td>
  1129. <td>${name2}</td>
  1130. <td>${fileInfo}</td>
  1131. <td title="Download this image."><button>DL</button></td>
  1132. </tr>`)
  1133. let tds = overlay.lastChild.children
  1134. if (z) tds[1].appendChild(z)
  1135. tds[4].addEventListener('click', dl)
  1136. }
  1137. }
  1138. }
  1139. }
  1140. function selectAll(check = true) {
  1141. for (let x of overlay.children) {
  1142. x.children[0].firstChild.checked = check
  1143. }
  1144. }
  1145. function selectAllOf(type) {
  1146. for (let x of overlay.children) {
  1147. x.children[0].firstChild.checked = x.children[2].innerText.startsWith(type)
  1148. }
  1149. }
  1150.  
  1151. function extensionFromMimeType(mime) {
  1152. let extension
  1153. switch (mime) {
  1154. case 'image/png':
  1155. extension = '.png'
  1156. break;
  1157. case 'image/webp':
  1158. extension = '.webp'
  1159. break;
  1160. case 'image/gif':
  1161. extension = '.gif'
  1162. break;
  1163. case 'image/avif':
  1164. extension = '.avif'
  1165. break;
  1166. case 'image/jxl':
  1167. extension = '.jxl'
  1168. break;
  1169. case 'image/svg+xml':
  1170. extension = '.svg'
  1171. break;
  1172. default:
  1173. extension = '.jpeg'
  1174. break
  1175. }
  1176. return extension
  1177. }
  1178. function createDownload(url, filename) {
  1179. let a = document.createElement('a')
  1180. a.href = url
  1181. a.download = filename
  1182. a.click()
  1183. }
  1184. function GM_xmlhttpRequest_asyncWrapper (urlToFetch) {
  1185. return new Promise((resolve, reject) => {
  1186. let url = new URL(urlToFetch, location.href)
  1187. let sameOrigin = urlToFetch.startsWith(location.origin)
  1188. GM_xmlhttpRequest({
  1189. url: url.href,
  1190. responseType: 'blob',
  1191. anonymous: false,
  1192. headers: {
  1193. 'Referer': location.origin + '/',
  1194. 'Sec-Fetch-Dest': 'image',
  1195. 'Sec-Fetch-Mode': 'no-cors',
  1196. 'Sec-Fetch-Site': sameOrigin ? 'same-origin' : 'cross-site',
  1197. 'Pragma': 'no-cache',
  1198. 'Cache-Control': 'no-cache'
  1199. },
  1200. onload: resolve,
  1201. onerror: reject
  1202. })
  1203. })
  1204. }
  1205. async function fetchImg(url) {
  1206. if (url.startsWith('http')) {
  1207. let r = await GM_xmlhttpRequest_asyncWrapper(url)
  1208. let mime = r.responseHeaders.match(/content-type: (.*)/i)
  1209. return {
  1210. data: await r.response,
  1211. contentType: mime && mime[1]
  1212. }
  1213. } else {
  1214. let r = await fetch(url, {cache: 'force-cache'})
  1215. return {
  1216. data: await r.blob(),
  1217. contentType: r.headers.get('content-type')
  1218. }
  1219. }
  1220. }
  1221.  
  1222. async function dl() {
  1223. const url = this.parentElement.children[1].firstChild.src
  1224. console.log(this, url)
  1225. let img = await fetchImg(url)
  1226. let extension = extensionFromMimeType(img.contentType)
  1227. let objurl = createUrlFromBlob(img.data)
  1228. createDownload(objurl, this.parentElement.children[2].innerText + extension)
  1229. }
  1230.  
  1231. async function dlSelected() {
  1232. let files = []
  1233. let filenames = []
  1234. // for (let x of overlay.children) {
  1235. // if (x.children[0].firstChild.checked) {
  1236. // try {
  1237. // let img, extension
  1238. // if (x.children[1].firstChild.toString() == "[object HTMLImageElement]") {
  1239. // const url = x.children[1].firstChild.src
  1240. // let req = await fetchImg(url)
  1241. // extension = extensionFromMimeType(req.contentType)
  1242. // img = req.data
  1243. // } else if (x.children[1].firstChild.toString() == "[object HTMLCanvasElement]") {
  1244. // img = await ctob(x.children[1].firstChild)
  1245. // extension = '.png'
  1246. // }
  1247. // let filename = x.children[2].innerText
  1248. // let filenameCount = filenames.reduce((a,x)=>a+(x==filename?1:0), 0)
  1249. // filenames.push(filename)
  1250. // if (filenameCount) filename += ' ('+(filenameCount+1)+')';
  1251. // files.push({
  1252. // name: filename + extension,
  1253. // data: await img.arrayBuffer()
  1254. // })
  1255. // } catch (e) {}
  1256. // }
  1257. // }
  1258. await Promise.allSettled(Array.from(overlay.children).map(async x => {
  1259. if (x.children[0].firstChild.checked) {
  1260. try {
  1261. let img, extension
  1262. if (x.children[1].firstChild.toString() == "[object HTMLImageElement]") {
  1263. const url = x.children[1].firstChild.src
  1264. let req = await fetchImg(url)
  1265. extension = extensionFromMimeType(req.contentType)
  1266. img = req.data
  1267. } else if (x.children[1].firstChild.toString() == "[object HTMLCanvasElement]") {
  1268. img = await ctob(x.children[1].firstChild)
  1269. extension = '.png'
  1270. }
  1271. let filename = x.children[2].innerText
  1272. let filenameCount = filenames.reduce((a,x)=>a+(x==filename?1:0), 0)
  1273. filenames.push(filename)
  1274. if (filenameCount) filename += ' ('+(filenameCount+1)+')';
  1275. files.push({
  1276. name: filename + extension,
  1277. data: await img.arrayBuffer()
  1278. })
  1279. } catch (e) {}
  1280. };
  1281. }))
  1282. let zipData = await NutZip(files)
  1283. let blob = new Blob([zipData], {type: "octet/stream"})
  1284. var url = createUrlFromBlob(blob);
  1285. createDownload(url, (+new Date())+'.zip')
  1286. }
  1287.  
  1288. async function dlAllImgs() {
  1289. let promises = []
  1290. let filenameCounter = 0
  1291. let fileCompletedCounter = 0
  1292. dledImgsCounterElement.innerText = '(0 imgs)'
  1293. async function getImg(x, i) {
  1294. const url = x.getAttribute('data-original') || x.getAttribute('data-src') || x.getAttribute('content')
  1295. let img
  1296. try {
  1297. img = await fetchImg(url)
  1298. } catch (e) {
  1299. img = await fetchImg(x.src)
  1300. }
  1301. let extension = extensionFromMimeType(img.contentType)
  1302. let filename = String(i).padStart(4, '0')
  1303. dledImgsCounterElement.innerText = `(${++fileCompletedCounter} imgs)`
  1304. return {
  1305. name: filename + extension,
  1306. data: await img.data.arrayBuffer()
  1307. }
  1308. }
  1309. for (let x of document.getElementsByTagName('img')) {
  1310. promises.push(getImg(x, ++filenameCounter))
  1311. }
  1312. Promise.allSettled(promises).then((p) => {
  1313. let files = []
  1314. p.map(x => (x.status == 'fulfilled') && files.push(x.value))
  1315. let zipData = SimpleZip.GenerateZipFrom(files)
  1316. let blob = new Blob([zipData], {type: "octet/stream"})
  1317. var url = createUrlFromBlob(blob);
  1318. createDownload(url, (+new Date())+'.zip')
  1319. dledImgsCounterElement.innerText = ''
  1320. })
  1321. }
  1322.  
  1323.  
  1324.  
  1325. const urlToBlobMapping = {}
  1326.  
  1327.  
  1328. window.addEventListener(OPTIONS.keys.toContext, (e) => {
  1329. switch (e.detail.action) {
  1330. case 'log':
  1331. logger(e.detail.title, e.detail.that, e.detail.args, e.detail.context)
  1332. break
  1333. case 'captureImage':
  1334. captureNewImage(e.detail)
  1335. break
  1336. case 'urlToBlob':
  1337. urlToBlobMapping[e.detail.url] = e.detail.blob
  1338. break
  1339. case 'ignoreSource':
  1340. ignoredSources.push(e.detail.source)
  1341. break
  1342. }
  1343. })
  1344.  
  1345. }
  1346.  
  1347. console.log('cr loaded')
  1348. })()