From 29e63296acd16576383d9495db908ce27da3e060 Mon Sep 17 00:00:00 2001 From: Steve Seguin Date: Tue, 26 Jul 2022 08:10:50 -0400 Subject: [PATCH] Add files via upload --- thirdparty/StreamSaver.js | 248 ++++++++++++++++++----------------- thirdparty/canvasFilters.js | 253 ++++++++++++++++++++++++++++++++++++ thirdparty/focus_worker.js | 11 ++ thirdparty/measureBlur.js | 124 ++++++++++++++++++ 4 files changed, 514 insertions(+), 122 deletions(-) create mode 100644 thirdparty/canvasFilters.js create mode 100644 thirdparty/focus_worker.js create mode 100644 thirdparty/measureBlur.js diff --git a/thirdparty/StreamSaver.js b/thirdparty/StreamSaver.js index fd549e2..41860f3 100644 --- a/thirdparty/StreamSaver.js +++ b/thirdparty/StreamSaver.js @@ -2,135 +2,34 @@ /* global chrome location ReadableStream define MessageChannel TransformStream */ -;((name, definition) => { - typeof module !== 'undefined' - ? module.exports = definition() - : typeof define === 'function' && typeof define.amd === 'object' - ? define(definition) - : this[name] = definition() -})('streamSaver', () => { +function streamSaverFunction(){ 'use strict' - const global = typeof window === 'object' ? window : this + const global = typeof window === 'object' ? window : this; if (!global.HTMLElement) console.warn('streamsaver is meant to run on browsers main thread') - let mitmTransporter = null - let supportsTransferable = false - const test = fn => { try { fn() } catch (e) {} } - const ponyfill = global.WebStreamsPolyfill || {} - const isSecureContext = global.isSecureContext + let mitmTransporter = null; + let supportsTransferable = false; + const test = fn => { try { fn() } catch (e) {} }; + const ponyfill = global.WebStreamsPolyfill || {}; + const isSecureContext = global.isSecureContext; + + //console.log(ponyfill); + //console.log(isSecureContext); + // TODO: Must come up with a real detection test (#69) - let useBlobFallback = /constructor/i.test(global.HTMLElement) || !!global.safari || !!global.WebKitPoint + let useBlobFallback = /constructor/i.test(global.HTMLElement) || !!global.safari || !!global.WebKitPoint; + + //console.log(useBlobFallback); + const downloadStrategy = isSecureContext || 'MozAppearance' in document.documentElement.style ? 'iframe' - : 'navigate' - - const streamSaver = { - createWriteStream, - WritableStream: global.WritableStream || ponyfill.WritableStream, - supported: true, - version: { full: '2.0.7', major: 2, minor: 0, dot: 7 }, - mitm: './thirdparty/mitm.html?v=2' - } - - /** - * create a hidden iframe and append it to the DOM (body) - * - * @param {string} src page to load - * @return {HTMLIFrameElement} page to load - */ - function makeIframe (src) { - if (!src) throw new Error('meh') - const iframe = document.createElement('iframe') - iframe.hidden = true - iframe.src = src - iframe.loaded = false - iframe.name = 'iframe' - iframe.isIframe = true - iframe.postMessage = (...args) => iframe.contentWindow.postMessage(...args) - iframe.addEventListener('load', () => { - iframe.loaded = true - }, { once: true }) - document.body.appendChild(iframe) - return iframe - } - - /** - * create a popup that simulates the basic things - * of what a iframe can do - * - * @param {string} src page to load - * @return {object} iframe like object - */ - function makePopup (src) { - const options = 'width=200,height=100' - const delegate = document.createDocumentFragment() - const popup = { - frame: global.open(src, 'popup', options), - loaded: false, - isIframe: false, - isPopup: true, - remove () { popup.frame.close() }, - addEventListener (...args) { delegate.addEventListener(...args) }, - dispatchEvent (...args) { delegate.dispatchEvent(...args) }, - removeEventListener (...args) { delegate.removeEventListener(...args) }, - postMessage (...args) { popup.frame.postMessage(...args) } - } - - const onReady = evt => { - if (evt.source === popup.frame) { - popup.loaded = true - global.removeEventListener('message', onReady) - popup.dispatchEvent(new Event('load')) - } - } - - global.addEventListener('message', onReady) - - return popup - } - - try { - // We can't look for service worker since it may still work on http - new Response(new ReadableStream()) - if (isSecureContext && !('serviceWorker' in navigator)) { - useBlobFallback = true - } - } catch (err) { - useBlobFallback = true - } - - test(() => { - // Transferable stream was first enabled in chrome v73 behind a flag - const { readable } = new TransformStream() - const mc = new MessageChannel() - mc.port1.postMessage(readable, [readable]) - mc.port1.close() - mc.port2.close() - supportsTransferable = true - // Freeze TransformStream object (can only work with native) - Object.defineProperty(streamSaver, 'TransformStream', { - configurable: false, - writable: false, - value: TransformStream - }) - }) - - function loadTransporter () { - if (!mitmTransporter) { - mitmTransporter = isSecureContext - ? makeIframe(streamSaver.mitm) - : makePopup(streamSaver.mitm) - } - } - - /** - * @param {string} filename filename that should be used - * @param {object} options [description] - * @param {number} size deprecated - * @return {WritableStream} - */ + : 'navigate'; + + //console.log(downloadStrategy); + function createWriteStream (filename, stopStream){ + //console.log("createWriteStream"); let opts = { size: null, pathname: null, @@ -200,6 +99,7 @@ } channel.port1.onmessage = evt => { + console.log(evt); // Service worker sent us a link that we should open. if (evt.data.download) { // Special treatment for popup... @@ -309,5 +209,109 @@ }, opts.writableStrategy) } + const streamSaver = { + createWriteStream, + WritableStream: global.WritableStream || ponyfill.WritableStream, + supported: true, + version: { full: '2.0.7', major: 2, minor: 0, dot: 7 }, + mitm: './thirdparty/mitm.html?v=2' + } + + //console.log(streamSaver); + + /** + * create a hidden iframe and append it to the DOM (body) + * + * @param {string} src page to load + * @return {HTMLIFrameElement} page to load + */ + function makeIframe (src) { + if (!src) throw new Error('meh') + const iframe = document.createElement('iframe') + iframe.hidden = true + iframe.src = src + iframe.loaded = false + iframe.name = 'iframe' + iframe.isIframe = true + iframe.postMessage = (...args) => iframe.contentWindow.postMessage(...args) + iframe.addEventListener('load', () => { + iframe.loaded = true + }, { once: true }) + document.body.appendChild(iframe) + return iframe + } + + /** + * create a popup that simulates the basic things + * of what a iframe can do + * + * @param {string} src page to load + * @return {object} iframe like object + */ + function makePopup (src) { + const options = 'width=200,height=100' + const delegate = document.createDocumentFragment() + const popup = { + frame: global.open(src, 'popup', options), + loaded: false, + isIframe: false, + isPopup: true, + remove () { popup.frame.close() }, + addEventListener (...args) { delegate.addEventListener(...args) }, + dispatchEvent (...args) { delegate.dispatchEvent(...args) }, + removeEventListener (...args) { delegate.removeEventListener(...args) }, + postMessage (...args) { popup.frame.postMessage(...args) } + } + + const onReady = evt => { + if (evt.source === popup.frame) { + popup.loaded = true + global.removeEventListener('message', onReady) + popup.dispatchEvent(new Event('load')) + } + } + + global.addEventListener('message', onReady) + + return popup + } + + try { + // We can't look for service worker since it may still work on http + new Response(new ReadableStream()) + if (isSecureContext && !('serviceWorker' in navigator)) { + useBlobFallback = true + } + } catch (err) { + useBlobFallback = true + } + + //console.log("useBlobFallback: "+useBlobFallback); + + test(() => { + // Transferable stream was first enabled in chrome v73 behind a flag + const { readable } = new TransformStream() + const mc = new MessageChannel() + mc.port1.postMessage(readable, [readable]) + mc.port1.close() + mc.port2.close() + supportsTransferable = true + // Freeze TransformStream object (can only work with native) + Object.defineProperty(streamSaver, 'TransformStream', { + configurable: false, + writable: false, + value: TransformStream + }) + }) + + function loadTransporter () { + if (!mitmTransporter) { + mitmTransporter = isSecureContext + ? makeIframe(streamSaver.mitm) + : makePopup(streamSaver.mitm) + } + } + return streamSaver -}) +}; +var streamSaver = streamSaverFunction(); \ No newline at end of file diff --git a/thirdparty/canvasFilters.js b/thirdparty/canvasFilters.js new file mode 100644 index 0000000..106dcf1 --- /dev/null +++ b/thirdparty/canvasFilters.js @@ -0,0 +1,253 @@ +// Modified copy obtained from https://github.com/timotgl/inspector-bokeh/tree/main/demo - MIT Lic +// Original file based on https://github.com/kig/canvasfilters/blob/master/filters.js +// I reduced the modified code to a few core functions; standard convolve/blur matrix functions. + +const Filters = {}; + +if (typeof Float32Array == 'undefined') { // good + Filters.getFloat32Array = Filters.getUint8Array = function (len) { + if (len.length) { + return len.slice(0); + } + return new Array(len); + }; +} else { + Filters.getFloat32Array = function (len) { + return new Float32Array(len); + }; + Filters.getUint8Array = function (len) { + return new Uint8Array(len); + }; +} + +if (typeof document != 'undefined') { + Filters.tmpCanvas = document.createElement('canvas'); + Filters.tmpCtx = Filters.tmpCanvas.getContext('2d'); + Filters.createImageData = function (w, h) { + return this.tmpCtx.createImageData(w, h); + }; +} else { + onmessage = function (e) { + var ds = e.data; + if (!ds.length) { + ds = [ds]; + } + postMessage(Filters.runPipeline(ds)); + }; + Filters.createImageData = function (w, h) { + return { width: w, height: h, data: this.getFloat32Array(w * h * 4) }; + }; +} + +Filters.convolve = function (pixels, weights, opaque) { // good + var side = Math.round(Math.sqrt(weights.length)); + var halfSide = Math.floor(side / 2); + + var src = pixels.data; + var sw = pixels.width; + var sh = pixels.height; + + var w = sw; + var h = sh; + var output = Filters.createImageData(w, h); + var dst = output.data; + + var alphaFac = opaque ? 1 : 0; + + for (var y = 0; y < h; y++) { + for (var x = 0; x < w; x++) { + var sy = y; + var sx = x; + var dstOff = (y * w + x) * 4; + var r = 0, + g = 0, + b = 0, + a = 0; + for (var cy = 0; cy < side; cy++) { + for (var cx = 0; cx < side; cx++) { + var scy = Math.min(sh - 1, Math.max(0, sy + cy - halfSide)); + var scx = Math.min(sw - 1, Math.max(0, sx + cx - halfSide)); + var srcOff = (scy * sw + scx) * 4; + var wt = weights[cy * side + cx]; + r += src[srcOff] * wt; + g += src[srcOff + 1] * wt; + b += src[srcOff + 2] * wt; + a += src[srcOff + 3] * wt; + } + } + dst[dstOff] = r; + dst[dstOff + 1] = g; + dst[dstOff + 2] = b; + dst[dstOff + 3] = a + alphaFac * (255 - a); + } + } + return output; +}; + + +Filters.luminance = function (pixels, args) { // good + var output = Filters.createImageData(pixels.width, pixels.height); + var dst = output.data; + var d = pixels.data; + for (var i = 0; i < d.length; i += 4) { + var r = d[i]; + var g = d[i + 1]; + var b = d[i + 2]; + // CIE luminance for the RGB + var v = 0.2126 * r + 0.7152 * g + 0.0722 * b; + dst[i] = dst[i + 1] = dst[i + 2] = v; + dst[i + 3] = d[i + 3]; + } + return output; +}; + +Filters.runPipeline = function (ds) { + var res = null; + res = this[ds[0].name].apply(this, ds[0].args); + for (var i = 1; i < ds.length; i++) { + var d = ds[i]; + var args = d.args.slice(0); + args.unshift(res); + res = this[d.name].apply(this, args); + } + return res; +}; + + +Filters.identity = function (pixels, args) { + var output = Filters.createImageData(pixels.width, pixels.height); + var dst = output.data; + var d = pixels.data; + for (var i = 0; i < d.length; i++) { + dst[i] = d[i]; + } + return output; +}; + + +Filters.horizontalConvolve = function (pixels, weightsVector, opaque) { + var side = weightsVector.length; + var halfSide = Math.floor(side / 2); + + var src = pixels.data; + var sw = pixels.width; + var sh = pixels.height; + + var w = sw; + var h = sh; + var output = Filters.createImageData(w, h); + var dst = output.data; + + var alphaFac = opaque ? 1 : 0; + + for (var y = 0; y < h; y++) { + for (var x = 0; x < w; x++) { + var sy = y; + var sx = x; + var dstOff = (y * w + x) * 4; + var r = 0, + g = 0, + b = 0, + a = 0; + for (var cx = 0; cx < side; cx++) { + var scy = sy; + var scx = Math.min(sw - 1, Math.max(0, sx + cx - halfSide)); + var srcOff = (scy * sw + scx) * 4; + var wt = weightsVector[cx]; + r += src[srcOff] * wt; + g += src[srcOff + 1] * wt; + b += src[srcOff + 2] * wt; + a += src[srcOff + 3] * wt; + } + dst[dstOff] = r; + dst[dstOff + 1] = g; + dst[dstOff + 2] = b; + dst[dstOff + 3] = a + alphaFac * (255 - a); + } + } + return output; +}; + +Filters.separableConvolve = function ( + pixels, + horizWeights, + vertWeights, + opaque +) { + return this.horizontalConvolve( + this.verticalConvolveFloat32(pixels, vertWeights, opaque), + horizWeights, + opaque + ); +}; + + +Filters.gaussianBlur = function (pixels, diameter) { // good + diameter = Math.abs(diameter); + if (diameter <= 1) return Filters.identity(pixels); + var radius = diameter / 2; + var len = Math.ceil(diameter) + (1 - (Math.ceil(diameter) % 2)); + var weights = this.getFloat32Array(len); + var rho = (radius + 0.5) / 3; + var rhoSq = rho * rho; + var gaussianFactor = 1 / Math.sqrt(2 * Math.PI * rhoSq); + var rhoFactor = -1 / (2 * rho * rho); + var wsum = 0; + var middle = Math.floor(len / 2); + for (var i = 0; i < len; i++) { + var x = i - middle; + var gx = gaussianFactor * Math.exp(x * x * rhoFactor); + weights[i] = gx; + wsum += gx; + } + for (var i = 0; i < weights.length; i++) { + weights[i] /= wsum; + } + return Filters.separableConvolve(pixels, weights, weights, false); +}; + + +Filters.verticalConvolveFloat32 = function (pixels, weightsVector, opaque) { + var side = weightsVector.length; + var halfSide = Math.floor(side / 2); + + var src = pixels.data; + var sw = pixels.width; + var sh = pixels.height; + + var w = sw; + var h = sh; + var output = { width: w, height: h, data: this.getFloat32Array(w * h * 4) }; + var dst = output.data; + + var alphaFac = opaque ? 1 : 0; + + for (var y = 0; y < h; y++) { + for (var x = 0; x < w; x++) { + var sy = y; + var sx = x; + var dstOff = (y * w + x) * 4; + var r = 0, + g = 0, + b = 0, + a = 0; + for (var cy = 0; cy < side; cy++) { + var scy = Math.min(sh - 1, Math.max(0, sy + cy - halfSide)); + var scx = sx; + var srcOff = (scy * sw + scx) * 4; + var wt = weightsVector[cy]; + r += src[srcOff] * wt; + g += src[srcOff + 1] * wt; + b += src[srcOff + 2] * wt; + a += src[srcOff + 3] * wt; + } + dst[dstOff] = r; + dst[dstOff + 1] = g; + dst[dstOff + 2] = b; + dst[dstOff + 3] = a + alphaFac * (255 - a); + } + } + return output; +}; + +export default Filters; diff --git a/thirdparty/focus_worker.js b/thirdparty/focus_worker.js new file mode 100644 index 0000000..3cac3bc --- /dev/null +++ b/thirdparty/focus_worker.js @@ -0,0 +1,11 @@ +// Part of Inspector Bokeh by @timotgl +// MIT License - Copyright (c) 2016 Timo Taglieber +// https://github.com/timotgl/inspector-bokeh + +import measureBlur from './measureBlur.js'; + +onmessage = (messageEvent) => { + postMessage({ + score: measureBlur(messageEvent.data.imageData), + }); +}; diff --git a/thirdparty/measureBlur.js b/thirdparty/measureBlur.js new file mode 100644 index 0000000..d438ccd --- /dev/null +++ b/thirdparty/measureBlur.js @@ -0,0 +1,124 @@ +// Inspector Bokeh by @timotgl +// MIT License - Copyright (c) 2016 Timo Taglieber +// https://github.com/timotgl/inspector-bokeh + +// This is just a copy of ../src/measureBlur.js that has been edited +// to assume that canvasFilters is already an ES module +// TODO: solve with bundling somehow + +import Filters from './canvasFilters.js'; + +/** + * I forgot why exactly I was doing this. + * It somehow improves edge detection to blur the image a bit beforehand. + * But we don't want to do this for very small images. + */ +const BLUR_BEFORE_EDGE_DETECTION_MIN_WIDTH = 360; // pixels +const BLUR_BEFORE_EDGE_DETECTION_DIAMETER = 5.0; // pixels + +/** + * Only count edges that reach a certain intensity. + * I forgot which unit this was. But it's not pixels. + */ +const MIN_EDGE_INTENSITY = 20; + +const detectEdges = (imageData) => { + const preBlurredImageData = + imageData.width >= BLUR_BEFORE_EDGE_DETECTION_MIN_WIDTH + ? Filters.gaussianBlur(imageData, BLUR_BEFORE_EDGE_DETECTION_DIAMETER) + : imageData; + + const greyscaled = Filters.luminance(preBlurredImageData); + const sobelKernel = Filters.getFloat32Array([1, 0, -1, 2, 0, -2, 1, 0, -1]); + return Filters.convolve(greyscaled, sobelKernel, true); +}; + +/** + * Reduce imageData from RGBA to only one channel (Y/luminance after conversion + * to greyscale) since RGB all have the same values and Alpha was ignored. + */ +const reducedPixels = (imageData) => { + const { data: pixels, width } = imageData; + const rowLen = width * 4; + let i, + x, + y, + row, + rows = []; + + for (y = 0; y < pixels.length; y += rowLen) { + row = new Uint8ClampedArray(imageData.width); + x = 0; + for (i = y; i < y + rowLen; i += 4) { + row[x] = pixels[i]; + x += 1; + } + rows.push(row); + } + return rows; +}; + +/** + * @param pixels Array of Uint8ClampedArrays (row in original image) + */ +const detectBlur = (pixels) => { + const width = pixels[0].length; + const height = pixels.length; + + let x, + y, + value, + oldValue, + edgeStart, + edgeWidth, + bm, + percWidth, + numEdges = 0, + sumEdgeWidths = 0; + + for (y = 0; y < height; y += 1) { + // Reset edge marker, none found yet + edgeStart = -1; + for (x = 0; x < width; x += 1) { + value = pixels[y][x]; + // Edge is still open + if (edgeStart >= 0 && x > edgeStart) { + oldValue = pixels[y][x - 1]; + // Value stopped increasing => edge ended + if (value < oldValue) { + // Only count edges that reach a certain intensity + if (oldValue >= MIN_EDGE_INTENSITY) { + edgeWidth = x - edgeStart - 1; + numEdges += 1; + sumEdgeWidths += edgeWidth; + } + edgeStart = -1; // Reset edge marker + } + } + // Edge starts + if (value == 0) { + edgeStart = x; + } + } + } + + if (numEdges === 0) { + bm = 0; + percWidth = 0; + } else { + bm = sumEdgeWidths / numEdges; + percWidth = (bm / width) * 100; + } + + return { + width: width, + height: height, + num_edges: numEdges, + avg_edge_width: bm, + avg_edge_width_perc: percWidth, + }; +}; + +const measureBlur = (imageData) => detectBlur(reducedPixels(detectEdges(imageData))); + +export default measureBlur;