├── static ├── issue-01-animations.css ├── images │ ├── logo.png │ ├── black.jpg │ ├── luxury.png │ ├── pixels.png │ ├── share.png │ ├── DCTGrid.jpg │ ├── corrupted.png │ ├── diamond.png │ ├── logo-nav.png │ ├── blanket-cat.jpg │ ├── glitchy-cat.gif │ ├── huff_simple.jpg │ ├── logo-bg-dark.png │ ├── blanket-cat-tiny.jpg │ ├── dct_coefficients.png │ ├── blanket-cat-notepad.png │ ├── compression-compare.png │ └── blanket-cat-smallest.jpg ├── issue-01-layout.css └── issue-01-theme.css ├── .vscode └── settings.json ├── scripts └── make-warc.sh ├── components ├── initialize.js ├── textarea.js ├── reference.js ├── image-fetch.js ├── raw-editor.js ├── full-dct-editor.js ├── image-canvas.js ├── dct-editor.js ├── chroma-editor.js ├── subsample-grid.js ├── editor-link.js └── utils │ ├── FastDct.js │ └── ImageUtilities.js ├── package.json ├── LICENSE ├── .gitignore ├── README.md ├── _local.html ├── _index.html ├── styles.css ├── data ├── DCT-grid.json ├── DCT-circle.json └── DCT-eye.json └── index.idyll /static/issue-01-animations.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ParametricPress/01-unraveling-the-jpeg/HEAD/static/images/logo.png -------------------------------------------------------------------------------- /static/images/black.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ParametricPress/01-unraveling-the-jpeg/HEAD/static/images/black.jpg -------------------------------------------------------------------------------- /static/images/luxury.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ParametricPress/01-unraveling-the-jpeg/HEAD/static/images/luxury.png -------------------------------------------------------------------------------- /static/images/pixels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ParametricPress/01-unraveling-the-jpeg/HEAD/static/images/pixels.png -------------------------------------------------------------------------------- /static/images/share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ParametricPress/01-unraveling-the-jpeg/HEAD/static/images/share.png -------------------------------------------------------------------------------- /static/images/DCTGrid.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ParametricPress/01-unraveling-the-jpeg/HEAD/static/images/DCTGrid.jpg -------------------------------------------------------------------------------- /static/images/corrupted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ParametricPress/01-unraveling-the-jpeg/HEAD/static/images/corrupted.png -------------------------------------------------------------------------------- /static/images/diamond.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ParametricPress/01-unraveling-the-jpeg/HEAD/static/images/diamond.png -------------------------------------------------------------------------------- /static/images/logo-nav.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ParametricPress/01-unraveling-the-jpeg/HEAD/static/images/logo-nav.png -------------------------------------------------------------------------------- /static/images/blanket-cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ParametricPress/01-unraveling-the-jpeg/HEAD/static/images/blanket-cat.jpg -------------------------------------------------------------------------------- /static/images/glitchy-cat.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ParametricPress/01-unraveling-the-jpeg/HEAD/static/images/glitchy-cat.gif -------------------------------------------------------------------------------- /static/images/huff_simple.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ParametricPress/01-unraveling-the-jpeg/HEAD/static/images/huff_simple.jpg -------------------------------------------------------------------------------- /static/images/logo-bg-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ParametricPress/01-unraveling-the-jpeg/HEAD/static/images/logo-bg-dark.png -------------------------------------------------------------------------------- /static/images/blanket-cat-tiny.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ParametricPress/01-unraveling-the-jpeg/HEAD/static/images/blanket-cat-tiny.jpg -------------------------------------------------------------------------------- /static/images/dct_coefficients.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ParametricPress/01-unraveling-the-jpeg/HEAD/static/images/dct_coefficients.png -------------------------------------------------------------------------------- /static/images/blanket-cat-notepad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ParametricPress/01-unraveling-the-jpeg/HEAD/static/images/blanket-cat-notepad.png -------------------------------------------------------------------------------- /static/images/compression-compare.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ParametricPress/01-unraveling-the-jpeg/HEAD/static/images/compression-compare.png -------------------------------------------------------------------------------- /static/images/blanket-cat-smallest.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ParametricPress/01-unraveling-the-jpeg/HEAD/static/images/blanket-cat-smallest.jpg -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "spellright.language": [ 3 | "en" 4 | ], 5 | "spellright.documentTypes": [ 6 | "markdown", 7 | "latex", 8 | "plaintext", 9 | "idyll" 10 | ], 11 | "spellright.parserByClass": { 12 | "idyll": { 13 | "parser": "plain" 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /scripts/make-warc.sh: -------------------------------------------------------------------------------- 1 | 2 | 3 | npm link parametric-components parametric-styles 4 | cd node_modules/parametric-components/ 5 | git checkout local-static 6 | npm run build 7 | cd ../parametric-styles 8 | git checkout local-static 9 | cd ../.. 10 | cp node_modules/parametric-styles/issue-01-layout.css ./static/ 11 | cp node_modules/parametric-styles/issue-01-theme.css ./static/ 12 | idyll build --template _local.html 13 | rm build.warc.gz 14 | warcit https://parametric.press/issue-01/unraveling-the-jpeg/ build/ 15 | -------------------------------------------------------------------------------- /components/initialize.js: -------------------------------------------------------------------------------- 1 | 2 | const React = require('react'); 3 | 4 | class Initialize extends React.Component { 5 | componentDidMount(e) { 6 | const body = document.getElementsByTagName('body')[0]; 7 | body.classList.add('enabled'); 8 | } 9 | componentWillUnmount(e) { 10 | const body = document.getElementsByTagName('body')[0]; 11 | body.classList.remove('enabled'); 12 | } 13 | render() { 14 | return null; 15 | } 16 | } 17 | 18 | module.exports = Initialize; 19 | -------------------------------------------------------------------------------- /components/textarea.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | class TextArea extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | this.onChange = this.onChange.bind(this); 7 | } 8 | onChange(e) { 9 | this.props.updateProps({ value: e.target.value }); 10 | } 11 | render() { 12 | const { hasError, idyll, updateProps, ...props } = this.props; 13 | return ( 14 | 15 | ); 16 | } 17 | } 18 | 19 | module.exports = TextArea; 20 | -------------------------------------------------------------------------------- /components/reference.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const ReactTooltip = require('react-tooltip'); 3 | 4 | let id = 0; 5 | class Tooltip extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | 9 | this._id = `parametric-reference-${id++}`; 10 | } 11 | render() { 12 | const { hasError, idyll, updateProps, children, ...props } = this.props; 13 | return ( 14 | 15 | {children} 16 | 17 | 18 | 19 | 20 | ); 21 | } 22 | } 23 | 24 | module.exports = Tooltip; 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unraveling-the-jpeg", 3 | "version": "1.0.0", 4 | "license": "MIT", 5 | "idyll": { 6 | "css": "styles.css", 7 | "authorView": false, 8 | "template": "_index.html", 9 | "layout": "none", 10 | "theme": "none", 11 | "components": [ 12 | "./components", 13 | "./node_modules/parametric-components/dist/cjs/issue-01" 14 | ] 15 | }, 16 | "dependencies": { 17 | "brace": "^0.11.1", 18 | "d3": "^4.0.0", 19 | "idyll": "^4.0.7", 20 | "idyll-d3-component": "^2.0.0", 21 | "idyll-document": "^3.0.6", 22 | "jpeg-js": "github:OmarShehata/jpeg-js", 23 | "parametric-components": "^1.0.33", 24 | "parametric-styles": "^1.0.21", 25 | "react-image": "^2.0.0", 26 | "whatwg-fetch": "^3.0.0" 27 | }, 28 | "devDependencies": { 29 | "gh-pages": "^0.12.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Omar Shehata 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /components/image-fetch.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | class ImageFetch extends React.Component { 4 | 5 | componentDidMount() { 6 | fetch(this.props.src) 7 | .then((response) => { 8 | return response.arrayBuffer(); 9 | }) 10 | .then((buffer) => { 11 | let bytes = new Uint8Array(buffer); 12 | let start = false; 13 | let saveBytes = []; 14 | let headerBytes = []; 15 | for (let i = 0; i < bytes.length; ++i) { 16 | // From: 17 | // Start is FFDA (255 218) 18 | // End is FFD9 (255 217) 19 | // If there exists a thumbnail, it will trip this up 20 | // Maybe find a way to check for a thumbnail and ignore it? 21 | if (i < bytes.length - 1 && bytes[i] == 255 && bytes[i+1] == 218) { 22 | start = true; 23 | } 24 | if (start) { 25 | //bytes[i] = parseInt(replacement[index++], 16); 26 | saveBytes.push(bytes[i]); 27 | } else { 28 | headerBytes.push(bytes[i]); 29 | } 30 | } 31 | this.props.updateProps({ 32 | header: headerBytes.join(' '), 33 | body: saveBytes.join(' ') 34 | }) 35 | }); 36 | } 37 | 38 | render() { 39 | return null; 40 | } 41 | } 42 | 43 | module.exports = ImageFetch; 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | .idyll 61 | build 62 | 63 | .vscode 64 | 65 | static/fonts 66 | 67 | _warc_cache/ 68 | *.warc.gz -------------------------------------------------------------------------------- /components/raw-editor.js: -------------------------------------------------------------------------------- 1 | const D3Component = require('idyll-d3-component'); 2 | 3 | class RawEditor extends D3Component { 4 | 5 | initialize(node, props) { 6 | // node is a
container, 7 | node.className = 'image-editor-container'; 8 | const ImageUtilities = require('./utils/ImageUtilities'); 9 | let that = this; 10 | 11 | function done(imageEditor) { 12 | imageEditor.createImageEditor(node); 13 | // Get the body. 14 | let body = imageEditor.body; 15 | let header = imageEditor.header; 16 | let total = header.concat(body); 17 | // Put it in, split by each 16 numbers 18 | if (imageEditor.showHeader) { 19 | imageEditor.putValuesInEditor(total, 16, true); 20 | } else { 21 | imageEditor.putValuesInEditor(body, 16, true); 22 | } 23 | 24 | 25 | setTimeout(() => imageEditor.editor.resize(), 1500) 26 | that.imageEditor = imageEditor; 27 | } 28 | 29 | new ImageUtilities({ 30 | url: props.imageUrl, 31 | showHeader: props.showHeader, 32 | corruptedImage: props.corruptedImage, 33 | editMode: 'raw', 34 | maxWidth: props.maxWidth, 35 | isUrlExempt: props.isUrlExempt, 36 | callback: done, 37 | }); 38 | 39 | } 40 | 41 | update(props, oldProps) { 42 | } 43 | 44 | } 45 | 46 | module.exports = RawEditor; -------------------------------------------------------------------------------- /components/full-dct-editor.js: -------------------------------------------------------------------------------- 1 | const D3Component = require('idyll-d3-component'); 2 | 3 | class FullDctEditor extends D3Component { 4 | 5 | initialize(node, props) { 6 | // node is a
container, 7 | 8 | const ImageUtilities = require('./utils/ImageUtilities'); 9 | node.className = 'image-editor-container'; 10 | 11 | let that = this; 12 | 13 | function done(imageEditor) { 14 | imageEditor.createImageEditor(node, 'Full Discrete Cosine Transform'); 15 | // Get the decoded luminance values. 16 | let luminanceValues = imageEditor.getDecodedComponent('Y'); 17 | // Convert them into cosine waves. 18 | let dctLuminance = imageEditor.forwardDct(luminanceValues, true); 19 | imageEditor.numberOfCoefficients = dctLuminance.length; 20 | 21 | // Put the coefficients in the editor. 22 | imageEditor.putValuesInEditor(dctLuminance, imageEditor.decodedImage.width, true); 23 | 24 | setTimeout(() => imageEditor.editor.resize(), 1500) 25 | 26 | that.imageEditor = imageEditor; 27 | } 28 | 29 | new ImageUtilities({ 30 | url: props.imageUrl, 31 | corruptedImage: props.corruptedImage, 32 | editMode: 'full-dctLuminance', 33 | maxWidth: props.maxWidth, 34 | isUrlExempt: props.isUrlExempt, 35 | callback: done 36 | }); 37 | 38 | } 39 | 40 | update(props, oldProps) { 41 | } 42 | 43 | } 44 | 45 | module.exports = FullDctEditor; -------------------------------------------------------------------------------- /components/image-canvas.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const D3Component = require('idyll-d3-component'); 3 | 4 | const size = 600; 5 | 6 | class CustomD3Component extends D3Component { 7 | 8 | initialize(node, props) { 9 | this.node = node; 10 | this.canvas = document.createElement('canvas'); 11 | this.node.appendChild(this.canvas); 12 | this.createImage = this.createImage.bind(this); 13 | 14 | // Fetch and save the "Image was corrupted!" placeholder. 15 | const that = this; 16 | fetch('static/images/corrupted.png') 17 | .then(function(response){ 18 | return response.blob(); 19 | }) 20 | .then(createImageBitmap) 21 | .then(function(imageBitmap) { 22 | that.corruptedImage = imageBitmap; 23 | }) 24 | } 25 | 26 | draw(imageBitmap, width, height) { 27 | const ctx = this.canvas.getContext('2d'); 28 | if (width && height) { 29 | this.canvas.width = width; 30 | this.canvas.height = height; 31 | } 32 | 33 | ctx.drawImage(imageBitmap, 0, 0, this.canvas.width, this.canvas.height); 34 | } 35 | 36 | createImage (header, body) { 37 | const byteArray = new Uint8Array((header + ' ' + body).split(' ').map(parseFloat)); 38 | let blob = new Blob([byteArray.buffer], {type: 'image/jpeg'}); 39 | const that = this; 40 | 41 | createImageBitmap(blob) 42 | .then((imageBitmap) => { 43 | that.draw(imageBitmap, imageBitmap.width, imageBitmap.height); 44 | }) 45 | .catch((error) => { 46 | that.draw(this.corruptedImage); 47 | }); 48 | } 49 | 50 | update(props, oldProps) { 51 | if (props.body !== oldProps.body || props.header !== oldProps.header) { 52 | this.createImage(props.header, props.body); 53 | } 54 | } 55 | } 56 | 57 | module.exports = CustomD3Component; 58 | -------------------------------------------------------------------------------- /components/dct-editor.js: -------------------------------------------------------------------------------- 1 | const D3Component = require('idyll-d3-component'); 2 | 3 | class DctEditor extends D3Component { 4 | 5 | initialize(node, props) { 6 | // node is a
container, 7 | 8 | const ImageUtilities = require('./utils/ImageUtilities'); 9 | node.className = 'image-editor-container'; 10 | let editMode = 'dctLuminance'; 11 | let comp = props.comp; 12 | 13 | if(comp == undefined) comp = 'Y'; 14 | if (props.comp == 'Cb') editMode = 'dctBlue'; 15 | if (props.comp == 'Cr') editMode = 'dctRed'; 16 | 17 | let that = this; 18 | 19 | function done(imageEditor) { 20 | imageEditor.createImageEditor(node, 'Discrete Cosine Transform'); 21 | 22 | if (props.override != undefined) { 23 | let values = []; 24 | let lines = props.override.content.split("\n"); 25 | for (let line of lines) { 26 | values = values.concat(line.trim().split(" ")); 27 | } 28 | imageEditor.putValuesInEditor(values, 64, true); 29 | 30 | } else { 31 | // Get the DCT coefficients out. 32 | let dctCoefficients = imageEditor.getDctComponent(comp); 33 | 34 | // Put each block in a line, since each block has 64 numbers. 35 | imageEditor.putValuesInEditor(dctCoefficients, 64, true); 36 | } 37 | 38 | that.imageEditor = imageEditor; 39 | setTimeout(() => imageEditor.editor.resize(), 1500) 40 | } 41 | 42 | new ImageUtilities({ 43 | url: props.imageUrl, 44 | corruptedImage: props.corruptedImage, 45 | highlightPixelOnClick: true, 46 | editMode: editMode, 47 | maxWidth: props.maxWidth, 48 | isUrlExempt: props.isUrlExempt, 49 | callback: done 50 | }); 51 | } 52 | 53 | update(props, oldProps) { 54 | } 55 | 56 | } 57 | 58 | module.exports = DctEditor; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unraveling the JPEG 2 | 3 | This is the source code for [Unraveling the JPEG](https://issue-01-preview.parametric.press/issue-01/unraveling-the-jpeg/), an interactive exploration of how to decode a JPEG image that was published in the first issue of the Parametric Press. 4 | 5 | See also [https://github.com/OmarShehata/jpeg-sandbox](https://github.com/OmarShehata/jpeg-sandbox) for a tool for editing the DCT coefficients directly. 6 | 7 | ![jpeg-sandbox2](https://user-images.githubusercontent.com/1711126/156947858-542614db-52f8-41b1-b377-c34e532453b3.gif) 8 | 9 | 10 | ## Replacing the images with your own 11 | 12 | You can append the query parameter `?imageSrc=https://urlToImage.jpg` to the article, and it will refresh and load a version of the article using that image instead of the cat picture. It should work as long as the server allows cross origin requests. 13 | 14 | The easiest way to put your own image in the article is to upload it to [Imgur](http://imgur.com/) and add the link to it as a query parameter. You can then share this link with anyone to essentially have your own custom version of the article! 15 | 16 | For example, I can go to [this photo of a fox](https://unsplash.com/photos/OU2vFQCwCD0) on Unplash, right click and "copy image location", then put that URL in a query parameter. [This link](https://issue-01-preview.parametric.press/issue-01/unraveling-the-jpeg/?imageSrc=https://images.unsplash.com/photo-1518526157563-b1ee37a05129?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1050&q=80) will then load the article with the fox image in all the interactive diagrams. 17 | 18 | ## Local Setup 19 | 20 | To run the article locally, make sure you have NodeJS and NPM installed. Then clone or download this repository. 21 | 22 | ### Installing dependencies 23 | 24 | 1. Install `idyll` globally (only need to do this once): `npm install -g idyll` 25 | 2. Install local dependencies: `npm install` 26 | 27 | ### Running local dev server 28 | 29 | 1. Run `idyll --template _local.html` in the root of this project. 30 | -------------------------------------------------------------------------------- /_local.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | {{#title}} 9 | {{title}} 10 | 11 | {{/title}} {{^title}} 12 | Idyll 13 | 14 | {{/title}} 15 | 16 | {{#favicon}} 17 | 18 | {{/favicon}} {{#shareImageUrl}} 19 | 20 | 21 | 22 | {{/shareImageUrl}} {{#shareImageWidth}} 23 | 24 | {{/shareImageWidth}} {{#shareImageHeight}} 25 | 26 | {{/shareImageHeight}} 27 | 28 | 29 | {{#description}} 30 | 31 | 32 | {{/description}} {{#url}} 33 | 34 | {{/url}} {{#twitterHandle}} 35 | 36 | {{/twitterHandle}} {{#usesTex}} 37 | 41 | {{/usesTex}} {{#googleFontsUrl}} 42 | 43 | {{/googleFontsUrl}} 44 | 45 | 46 | 47 | 48 | 49 | 50 |
{{{idyllContent}}}
51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /_index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | {{#title}} 9 | {{title}} 10 | 11 | {{/title}} {{^title}} 12 | Idyll 13 | 14 | {{/title}} 15 | 16 | {{#favicon}} 17 | 18 | {{/favicon}} {{#shareImageUrl}} 19 | 20 | 21 | 22 | {{/shareImageUrl}} {{#shareImageWidth}} 23 | 24 | {{/shareImageWidth}} {{#shareImageHeight}} 25 | 26 | {{/shareImageHeight}} 27 | 28 | 29 | {{#description}} 30 | 31 | 32 | {{/description}} {{#url}} 33 | 34 | {{/url}} {{#twitterHandle}} 35 | 36 | {{/twitterHandle}} {{#usesTex}} 37 | 41 | {{/usesTex}} {{#googleFontsUrl}} 42 | 43 | {{/googleFontsUrl}} 44 | 45 | 46 | 47 | 48 | 49 | 50 |
{{{idyllContent}}}
51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /components/chroma-editor.js: -------------------------------------------------------------------------------- 1 | const D3Component = require('idyll-d3-component'); 2 | 3 | class ChromaEditor extends D3Component { 4 | 5 | initialize(node, props) { 6 | // node is a
container, 7 | 8 | const ImageUtilities = require('./utils/ImageUtilities'); 9 | node.className = 'image-editor-container'; 10 | let that = this; 11 | 12 | function done(imageEditor) { 13 | imageEditor.createImageEditor(node, 'Chrominance Subsampling'); 14 | 15 | // This assumes Y has a scale 1, and Cb and Cr have the same scale. 16 | // Interleave the colors based on the h and v of each component 17 | // Put each scan line on a line 18 | let colors = []; 19 | let components = imageEditor.decodedImage._decoder.components; 20 | let Y = components[0]; 21 | let Cb = components[1]; 22 | let Cr = components[2]; 23 | let scaleY = Math.round(Y.lines.length / Cb.lines.length); 24 | let scaleX = Math.round(Y.lines[0].length / Cb.lines[0].length); 25 | let scale = scaleX * scaleY; 26 | let samplesPerLine = Y.lines[0].length + Cb.lines[0].length + Cr.lines[0].length; 27 | imageEditor.samplesPerLine = samplesPerLine; 28 | imageEditor.scale = scale; 29 | 30 | let Cindex = 0; 31 | Y = imageEditor.getDecodedComponent('Y'); 32 | Cb = imageEditor.getDecodedComponent('Cb'); 33 | Cr = imageEditor.getDecodedComponent('Cr'); 34 | 35 | for (let i = 0; i < Y.length; i += scale) { 36 | // We have more Y than we have Cb and Cr 37 | // so we put multiple Y for each Cb and Cr 38 | for (let j = 0; j < scale; j++) { 39 | colors.push(Y[i + j]); 40 | } 41 | colors.push(Cb[Cindex]); 42 | colors.push(Cr[Cindex]); 43 | Cindex++; 44 | } 45 | imageEditor.putValuesInEditor(colors, samplesPerLine, true); 46 | 47 | setTimeout(() => imageEditor.editor.resize(), 1500) 48 | that.imageEditor = imageEditor; 49 | } 50 | 51 | let imageEditor = new ImageUtilities({ 52 | url: props.imageUrl, 53 | corruptedImage: props.corruptedImage, 54 | editMode: 'chroma', 55 | maxWidth: props.maxWidth, 56 | isUrlExempt: props.isUrlExempt, 57 | callback: done 58 | }); 59 | 60 | } 61 | 62 | update(props, oldProps) { 63 | } 64 | 65 | } 66 | 67 | module.exports = ChromaEditor; -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | 2 | .idyll-text-container img { 3 | /* border: 1px solid black; */ 4 | } 5 | .idyll-text-container canvas { 6 | border: 1px solid black; 7 | } 8 | 9 | .image-editor-container { 10 | font-family: 'Graphik Web'; 11 | background: #222; 12 | margin: 2em 0; 13 | } 14 | 15 | .image-editor-header { 16 | display: flex; 17 | flex-direction: row; 18 | justify-content: space-between; 19 | color: white; 20 | align-items: center; 21 | padding: 0.25em 0.5em; 22 | } 23 | 24 | .image-editor-header button { 25 | margin: 0; 26 | margin-left: 50px; 27 | padding: 0.5em 1em; 28 | transition: 0.25s; 29 | } 30 | .image-editor-header button:active { 31 | transform: scale(0.95); 32 | color: #c5c5c5; 33 | } 34 | 35 | .image-editor-header > * { 36 | flex: 1; 37 | } 38 | .image-editor-header > div:nth-of-type(1) { 39 | text-align: left; 40 | } 41 | .image-editor-header > div:nth-of-type(2) { 42 | text-align: center; 43 | } 44 | .image-editor-header > div:nth-of-type(3) { 45 | text-align: right; 46 | font-size: 14px; 47 | line-height: 16px; 48 | font-weight: 500; 49 | } 50 | 51 | .image-editor { 52 | display: flex; 53 | min-height: 400px; 54 | } 55 | .byte-editor { 56 | width: 100%; 57 | display: flex; 58 | flex-direction: column; 59 | } 60 | .ace-editor { 61 | flex-grow: 1; 62 | min-height: 200px; 63 | } 64 | .image-viewer { 65 | width:100%; 66 | display: flex; 67 | align-items: center; 68 | justify-content: center; 69 | } 70 | 71 | .image-viewer canvas { 72 | max-width:100%; 73 | /* width: 100%; 74 | height: 100%; */ 75 | } 76 | 77 | .replace-link { 78 | cursor:pointer; 79 | color: #4801FF; 80 | /* text-decoration: underline; */ 81 | border-bottom: dotted 1px #4801FF; 82 | } 83 | 84 | .replace-link:hover { 85 | color: #4801FF; 86 | } 87 | 88 | .reference { 89 | cursor:pointer; 90 | color: #222; 91 | text-decoration: underline; 92 | } 93 | 94 | 95 | .parametric-header-image img { 96 | left: -150px; 97 | width: 400px; 98 | } 99 | 100 | 101 | 102 | @media all and (max-width: 1000px) { 103 | .idyll-root { 104 | max-width: none; 105 | padding: 0; 106 | } 107 | } 108 | 109 | 110 | @media all and (max-width: 800px) { 111 | .idyll-root { 112 | max-width: none; 113 | padding: 0; 114 | } 115 | 116 | .image-editor { 117 | flex-direction: column; 118 | } 119 | .image-editor-header > div:nth-of-type(2) { 120 | line-height: 16px; 121 | font-size: 14px; 122 | } 123 | .image-editor-header > div:nth-of-type(3) { 124 | text-align: right; 125 | font-size: 10px; 126 | line-height: 12px; 127 | font-weight: 500; 128 | } 129 | .image-editor-header button { 130 | margin-left: 0; 131 | } 132 | 133 | } 134 | 135 | .row { 136 | display: flex; 137 | flex-direction: row; 138 | flex-wrap: wrap; 139 | width: 100%; 140 | } 141 | 142 | .column { 143 | display: flex; 144 | flex-direction: column; 145 | flex-basis: 100%; 146 | flex: 1; 147 | } 148 | 149 | .subsample-image { 150 | text-align: center; 151 | } 152 | .subsample-image p { 153 | margin-top:0px; 154 | } 155 | 156 | .subsample-image canvas { 157 | width: 200px; 158 | } 159 | 160 | .subsample-grid { 161 | background: none; 162 | } 163 | 164 | .subsample-controls { 165 | font-family: 'Graphik Web'; 166 | font-size: 14px; 167 | text-align: center; 168 | line-height: 1.1; 169 | margin-bottom: 2em; 170 | } 171 | .subsample-controls input { 172 | display: block; 173 | margin: 1em auto; 174 | } 175 | 176 | body.enabled { 177 | opacity: 1; 178 | } 179 | body.enabled .parametric-article-nav { 180 | opacity: 1; 181 | } 182 | body.enabled .idyll-text-container { 183 | opacity: 1; 184 | } 185 | body.enabled .article-header { 186 | opacity: 1; 187 | } 188 | body.enabled .parametric-article-dek { 189 | opacity: 1; 190 | } 191 | body.enabled .parametric-header-hed-text { 192 | opacity: 1; 193 | } 194 | body.enabled .hed-rotate { 195 | opacity: 1; 196 | } 197 | 198 | .parametric-tooltip { 199 | pointer-events: all; 200 | } -------------------------------------------------------------------------------- /components/subsample-grid.js: -------------------------------------------------------------------------------- 1 | const D3Component = require('idyll-d3-component'); 2 | 3 | function resetData(editor, originalData, savedComponents) { 4 | editor.decodedImage.data = originalData; 5 | 6 | editor.fillDecodedComponent('Y', savedComponents['Y']); 7 | editor.fillDecodedComponent('Cb', savedComponents['Cb']); 8 | editor.fillDecodedComponent('Cr', savedComponents['Cr']); 9 | 10 | editor.decodedImage._decoder.copyToImageData({ 11 | width: editor.decodedImage.width, 12 | height: editor.decodedImage.height, 13 | data: originalData 14 | }); 15 | } 16 | 17 | function redrawAll(scale, editor, originalData, savedComponents) { 18 | // RGB 19 | editor.canvas = document.querySelector("#R>canvas"); 20 | resetData(editor, originalData, savedComponents); 21 | editor.subsampleAndRedraw('R', scale) 22 | 23 | editor.canvas = document.querySelector("#G>canvas"); 24 | resetData(editor, originalData, savedComponents); 25 | editor.subsampleAndRedraw('G', scale) 26 | 27 | editor.canvas = document.querySelector("#B>canvas"); 28 | resetData(editor, originalData, savedComponents); 29 | editor.subsampleAndRedraw('B', scale) 30 | 31 | // YCbCr 32 | editor.canvas = document.querySelector("#Y>canvas"); 33 | resetData(editor, originalData, savedComponents); 34 | editor.subsampleAndRedraw('Y', scale) 35 | 36 | editor.canvas = document.querySelector("#Cb>canvas"); 37 | resetData(editor, originalData, savedComponents); 38 | editor.subsampleAndRedraw('Cb', scale) 39 | 40 | editor.canvas = document.querySelector("#Cr>canvas"); 41 | resetData(editor, originalData, savedComponents); 42 | editor.subsampleAndRedraw('Cr', scale) 43 | } 44 | 45 | class SubsampleGrid extends D3Component { 46 | 47 | initialize(node, props) { 48 | // node is a
container, 49 | 50 | const ImageUtilities = require('./utils/ImageUtilities'); 51 | node.className = 'image-editor-container subsample-grid'; 52 | let that = this; 53 | 54 | function done(imageEditor) { 55 | // Create 2x3 grid inside `node` 56 | let outerContainer = node; 57 | let container = document.createElement('div'); 58 | container.className = 'subsample-container row'; 59 | let html = `
60 |
61 |

Red

62 |

Luminance

63 |
64 |
65 |

Green

66 |

Blue Chrominance

67 |
68 |
69 |

Blue

70 |

Red Chrominance

71 |
72 |
`; 73 | container.innerHTML = html; 74 | node.appendChild(container); 75 | 76 | that.originalData = imageEditor.decodedImage.data.slice(); 77 | that.savedComponents['Y'] = imageEditor.getDecodedComponent('Y'); 78 | that.savedComponents['Cb'] = imageEditor.getDecodedComponent('Cb'); 79 | that.savedComponents['Cr'] = imageEditor.getDecodedComponent('Cr'); 80 | 81 | that.update(props); 82 | that.imageEditor = imageEditor; 83 | 84 | redrawAll(4, that.imageEditor, that.originalData, that.savedComponents); 85 | } 86 | 87 | new ImageUtilities({ 88 | url: props.imageUrl, 89 | editMode: 'subsample', 90 | maxWidth: props.maxWidth, 91 | isUrlExempt: props.isUrlExempt, 92 | callback: done 93 | }); 94 | 95 | this.savedComponents = {}; 96 | } 97 | 98 | update(props, oldProps) { 99 | if (this.imageEditor == undefined) return; 100 | let subsample = props.subsamplePercent; 101 | let scaleArray = [1, 2, 4, 6, 8, 10, 12, 14, 16, 20, 30, 40, 50, 80, 100, 0]; 102 | let scale = scaleArray[0 | (subsample * (scaleArray.length - 1))]; 103 | redrawAll(scale, this.imageEditor, this.originalData, this.savedComponents); 104 | } 105 | 106 | } 107 | 108 | module.exports = SubsampleGrid; -------------------------------------------------------------------------------- /static/issue-01-layout.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | } 4 | 5 | .idyll-root { 6 | box-sizing: border-box; 7 | margin: 0 auto; 8 | padding: 60px 0; 9 | margin-bottom: 60px; 10 | } 11 | 12 | .idyll-text-container { 13 | max-width: 600px; 14 | margin-top: 0; 15 | margin-right: 0; 16 | margin-bottom: 0; 17 | margin-left: 50px; 18 | } 19 | 20 | .section { 21 | padding: 0 10px; 22 | margin: 0 auto; 23 | } 24 | 25 | .article-header { 26 | width: 600px; 27 | width: 100%; 28 | text-align: left; 29 | padding-left: 50px; 30 | margin-bottom: 45px; 31 | } 32 | 33 | .inset { 34 | max-width: 400px; 35 | margin: 0 auto; 36 | } 37 | 38 | input { 39 | cursor: pointer; 40 | } 41 | 42 | .relative { 43 | position: relative; 44 | } 45 | .aside-container { 46 | position: relative; 47 | } 48 | .aside { 49 | position: absolute; 50 | width: 300px; 51 | right: calc((10vw + 600px + 150px) / -2); 52 | } 53 | 54 | .fixed { 55 | position: fixed; 56 | display: flex; 57 | align-self: center; 58 | flex-direction: column; 59 | align-items: center; 60 | right: 25px; 61 | top: 0; 62 | bottom: 0; 63 | width: calc((80vw - 600px) - 50px); 64 | justify-content: center; 65 | } 66 | 67 | .fixed div { 68 | width: 100%; 69 | } 70 | 71 | .idyll-scroll-graphic { 72 | position: -webkit-sticky; 73 | position: sticky; 74 | } 75 | 76 | .idyll-scroll-graphic img { 77 | max-height: 100vh; 78 | } 79 | 80 | .component-debug-view { 81 | position: relative; 82 | transition: background-color 0.3s ease-in; 83 | box-shadow: 5px 5px 10px 1px lightGray; 84 | } 85 | 86 | .author-view-button { 87 | position: absolute; 88 | top: 3px; 89 | right: 0; 90 | opacity: .38; 91 | background-color: #E7E3D0; 92 | background-image: url('https://idyll-lang.org/static/images/quill-icon.png'); 93 | background-repeat: no-repeat; 94 | background-size: contain; 95 | width: 24px; 96 | height: 24px; 97 | margin-right: 10px; 98 | box-sizing: border-box; 99 | border-radius: 12px; 100 | cursor: pointer; 101 | } 102 | 103 | .author-view-button:focus { 104 | outline: none; 105 | } 106 | 107 | .component-debug-view:hover > .author-view-button { 108 | opacity: 0.87; 109 | transition: opacity 600ms linear; 110 | } 111 | 112 | .author-component-view { 113 | display: flex; 114 | flex-direction: column; 115 | overflow-x: scroll; 116 | } 117 | 118 | .author-component-view h2, .author-component-view h3 { 119 | margin-top: 5px; 120 | margin-bottom: 5px; 121 | } 122 | 123 | .props-table { 124 | width: 90%; 125 | min-width: 500px; 126 | display: table; 127 | border: 1px solid #A4A2A2; 128 | border-radius: 20px; 129 | margin: 0 auto; 130 | } 131 | 132 | .props-table-type { 133 | font-family: 'Courier-New'; 134 | } 135 | 136 | .props-table-row { 137 | text-align: center; 138 | } 139 | 140 | .debug-collapse { 141 | overflow: hidden; 142 | overflow-y: scroll; 143 | transition: height 0.3s ease-in; 144 | margin: 0; 145 | box-sizing: border-box; 146 | } 147 | 148 | .icon-links { 149 | margin-top: 13px; 150 | text-align: center; 151 | display: flex; 152 | flex-direction: row; 153 | justify-content: center; 154 | } 155 | 156 | .icon-link { 157 | color: inherit; 158 | } 159 | 160 | .icon-link:hover { 161 | text-decoration: none; 162 | } 163 | 164 | .icon-link-image { 165 | cursor: pointer; 166 | } 167 | 168 | .button-tooltip { 169 | background-color: black !important; 170 | padding: 0 5px; 171 | } 172 | 173 | .button-tooltip.place-top:after { 174 | border-top-color: black !important; 175 | } 176 | 177 | .button-tooltip.place-right:after { 178 | border-right-color: black !important; 179 | } 180 | 181 | .button-tooltip.place-bottom:after { 182 | border-bottom-color: black !important; 183 | } 184 | 185 | .button-tooltip.place-left:after { 186 | border-left-color: black !important; 187 | } 188 | 189 | .tooltip-header { 190 | line-height: 1; 191 | margin: 6px 0; 192 | font-size: 18px; 193 | } 194 | 195 | .tooltip-subtitle { 196 | font-style: italic; 197 | } 198 | 199 | @media all and (max-width: 1600px) { 200 | .fixed { 201 | width: calc((85vw - 600px) - 50px); 202 | } 203 | } 204 | 205 | @media all and (max-width: 1000px) { 206 | /* put your css styles in here */ 207 | .desktop { 208 | display: none; 209 | } 210 | .relative { 211 | position: static; 212 | } 213 | .aside { 214 | position: static; 215 | width: 100%; 216 | right: 0; 217 | } 218 | .idyll-text-container { 219 | max-width: calc(100% - 2em); 220 | margin-top: 0; 221 | margin-right: 1em; 222 | margin-bottom: 0; 223 | margin-left: 1em; 224 | } 225 | .hed { 226 | width: 100%; 227 | } 228 | 229 | .idyll-root { 230 | padding: 15px 0; 231 | } 232 | 233 | .idyll-root { 234 | margin: 0 auto; 235 | padding-bottom: 80vh; 236 | } 237 | .article-header { 238 | margin: 0 auto; 239 | padding-left: 1em; 240 | } 241 | .fixed { 242 | position: fixed; 243 | left: 0; 244 | right: 0; 245 | bottom: 0; 246 | width: 100vw; 247 | top: initial; 248 | background: white; 249 | padding: 20px 0; 250 | border-top: solid 2px black; 251 | } 252 | } -------------------------------------------------------------------------------- /components/editor-link.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | class TextArea extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | this.onClick = this.onClick.bind(this); 7 | } 8 | onClick(e) { 9 | const { search, replace, line, editor, pattern, scale, DCT } = this.props; 10 | const imageEditor = editor.component.imageEditor; 11 | let text = imageEditor.resetText; 12 | //let text = imageEditor.editor.getSession().getValue(); 13 | let lines = text.split('\n'); 14 | let sentValuesToEditor = false; 15 | if (pattern == undefined) { 16 | for (let i = 0; i < lines.length; i++) { 17 | if (i == line - 1) { 18 | lines[i] = lines[i].replace(search, replace); 19 | break; 20 | } 21 | } 22 | } else if (pattern == "zero-all") { 23 | for (let i = 0; i < lines.length; i++) { 24 | let numbers = lines[i].trim().split(" "); 25 | for (let j = 0; j < numbers.length; j++) { 26 | numbers[j] = "0"; 27 | } 28 | lines[i] = numbers.join(" "); 29 | } 30 | } else if (pattern == "remove-zeros") { 31 | // For each line, find the last non-zero value, then remove all remaining zeros 32 | for (let i = 0; i < lines.length; i++) { 33 | let reg = /(-?[1-9]0*)+/g; 34 | let match; 35 | let lastIndex = 0; 36 | let lastLength = 1; 37 | while ((match = reg.exec(lines[i])) != null) { 38 | lastIndex = match.index; 39 | lastLength = match[0].length; 40 | } 41 | lines[i] = lines[i].slice(0, lastIndex + lastLength); 42 | } 43 | } else if (pattern.indexOf("remove-{") != -1) { 44 | // Remove the last n lines 45 | let n = pattern.match(/{(\d+)}/)[1]; 46 | lines = lines.slice(0, lines.length-n); 47 | } else if (pattern.indexOf("zero-{") != -1) { 48 | // Zero out the first n numbers 49 | let n = Number(pattern.match(/{(\d+)}/)[1]); 50 | let first = true; 51 | for (let i = 0; i < lines.length; i++) { 52 | let numbers = lines[i].trim().split(" "); 53 | for (let j = 0; j < numbers.length; j++) { 54 | if (!first) { 55 | numbers[j] = "0"; 56 | } 57 | first = false; 58 | n --; 59 | if (n <= 0) break; 60 | } 61 | 62 | lines[i] = numbers.join(" "); 63 | } 64 | } else if (pattern == 'animate-DCT') { 65 | let that = this; 66 | 67 | if (window.currentInterval != undefined && this.interval == window.currentInterval) { 68 | // Just pause 69 | clearInterval(window.currentInterval); 70 | window.currentInterval = undefined; 71 | } else { 72 | let values = []; 73 | let lines = DCT.content.split("\n"); 74 | for (let line of lines) { 75 | values = values.concat(line.trim().split(" ")); 76 | } 77 | 78 | if (JSON.stringify(values) != JSON.stringify(imageEditor.finalValues)) { 79 | // This is a new image! 80 | imageEditor.currentValues = undefined; 81 | } 82 | 83 | imageEditor.finalValues = values; 84 | 85 | // If it's already been going, and not done yet 86 | if (imageEditor.currentValues != undefined && imageEditor.currentValues.length < imageEditor.finalValues.length) { 87 | // No need to reset 88 | } else { 89 | // Otherwise, start from beginning 90 | imageEditor.setResetText(DCT.content); 91 | imageEditor.currentValues = []; 92 | } 93 | 94 | imageEditor.putValuesInEditor(imageEditor.currentValues, 64, false); 95 | if (window.currentInterval) { 96 | clearInterval(window.currentInterval); 97 | } 98 | window.currentInterval = setInterval(function(){ 99 | if (imageEditor.currentValues.length < imageEditor.finalValues.length) { 100 | // Add as many non trivial numbers. Non trivial is defined as less than 10. 101 | for (let i = imageEditor.currentValues.length; i < imageEditor.finalValues.length; i++) { 102 | imageEditor.currentValues.push(imageEditor.finalValues[i]); 103 | 104 | if (Math.abs(imageEditor.finalValues[i]) > 10) { 105 | break; 106 | } 107 | } 108 | 109 | imageEditor.putValuesInEditor(imageEditor.currentValues, 64, false); 110 | } else { 111 | clearInterval(window.currentInterval); 112 | window.currentInterval = undefined; 113 | } 114 | }, 20); 115 | this.interval = window.currentInterval; 116 | } 117 | 118 | sentValuesToEditor = true; 119 | } else { 120 | let count = 0; 121 | let values = imageEditor.getValuesFromEditor(imageEditor.resetText); 122 | let newValues = []; 123 | for (let i = 0; i < values.length; i+= scale + 2) { 124 | for (let j = 0; j < scale; j++) { 125 | let lookAheadCb = values[i + scale]; 126 | let lookAheadCr = values[i + scale + 1]; 127 | 128 | let Y = (pattern == 'isolate-Y') ? values[i + j] : 128; 129 | newValues.push(Y) 130 | } 131 | 132 | let Cb = (pattern == 'isolate-Cb') ? (values[i + scale]) : 128; 133 | let Cr = (pattern == 'isolate-Cr') ? (values[i + scale + 1]) : 128; 134 | 135 | newValues.push(Cb) 136 | newValues.push(Cr); 137 | } 138 | 139 | let samplesPerLine = imageEditor.samplesPerLine; 140 | imageEditor.putValuesInEditor(newValues, samplesPerLine); 141 | sentValuesToEditor = true; 142 | } 143 | 144 | if (!sentValuesToEditor) { 145 | text = lines.join('\n'); 146 | imageEditor.editor.setValue(text, -1); 147 | imageEditor.editor.scrollToLine(line - 1); 148 | } 149 | 150 | 151 | // this.props.updateProps({ value: e.target.value }); 152 | } 153 | render() { 154 | const { hasError, idyll, updateProps, ...props } = this.props; 155 | return ( 156 | {props.children} 157 | ); 158 | } 159 | } 160 | 161 | module.exports = TextArea; 162 | -------------------------------------------------------------------------------- /data/DCT-grid.json: -------------------------------------------------------------------------------- 1 | { 2 | "content": "254 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n0 254 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n0 0 254 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n0 0 0 254 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n0 0 0 0 254 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n0 0 0 0 0 254 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 254 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 254 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 254 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0 254 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0 0 254 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0 0 0 254 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0 0 0 0 254 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0 0 0 0 0 254 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0 0 0 0 0 0 254 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 254 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 254 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 254 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 254 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 254 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 254 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 254 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 254 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 254 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 254 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 254 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 254 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 254 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 254 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 254 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 254 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 254 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 254 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 254 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 254 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 254 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 254 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 254 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 254 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 254 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 254 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 254 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 254 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 254 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 254 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 254 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 254 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 254 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 254 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 254 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 254 0 0 0 0 0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 254 0 0 0 0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 254 0 0 0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 254 0 0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 254 0 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 254 0 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 254 0 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 254 0 0 0 0 0 0\n0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 254 0 0 0 0 0\n0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 254 0 0 0 0\n0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 254 0 0 0\n0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 254 0 0\n0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 254 0\n0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 254" 3 | } -------------------------------------------------------------------------------- /components/utils/FastDct.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Fast discrete cosine transform algorithms (JavaScript) 3 | * 4 | * Copyright (c) 2019 Project Nayuki. (MIT License) 5 | * https://www.nayuki.io/page/fast-discrete-cosine-transform-algorithms 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 8 | * this software and associated documentation files (the "Software"), to deal in 9 | * the Software without restriction, including without limitation the rights to 10 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 11 | * the Software, and to permit persons to whom the Software is furnished to do so, 12 | * subject to the following conditions: 13 | * - The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * - The Software is provided "as is", without warranty of any kind, express or 16 | * implied, including but not limited to the warranties of merchantability, 17 | * fitness for a particular purpose and noninfringement. In no event shall the 18 | * authors or copyright holders be liable for any claim, damages or other 19 | * liability, whether in an action of contract, tort or otherwise, arising from, 20 | * out of or in connection with the Software or the use or other dealings in the 21 | * Software. 22 | */ 23 | 24 | "use strict"; 25 | 26 | 27 | var fastDct8 = new function() { 28 | 29 | // DCT type II, scaled. Algorithm by Arai, Agui, Nakajima, 1988. 30 | // See: https://web.stanford.edu/class/ee398a/handouts/lectures/07-TransformCoding.pdf#page=30 31 | this.transform = function(vector) { 32 | var v0 = vector[0] + vector[7]; 33 | var v1 = vector[1] + vector[6]; 34 | var v2 = vector[2] + vector[5]; 35 | var v3 = vector[3] + vector[4]; 36 | var v4 = vector[3] - vector[4]; 37 | var v5 = vector[2] - vector[5]; 38 | var v6 = vector[1] - vector[6]; 39 | var v7 = vector[0] - vector[7]; 40 | 41 | var v8 = v0 + v3; 42 | var v9 = v1 + v2; 43 | var v10 = v1 - v2; 44 | var v11 = v0 - v3; 45 | var v12 = -v4 - v5; 46 | var v13 = (v5 + v6) * A[3]; 47 | var v14 = v6 + v7; 48 | 49 | var v15 = v8 + v9; 50 | var v16 = v8 - v9; 51 | var v17 = (v10 + v11) * A[1]; 52 | var v18 = (v12 + v14) * A[5]; 53 | 54 | var v19 = -v12 * A[2] - v18; 55 | var v20 = v14 * A[4] - v18; 56 | 57 | var v21 = v17 + v11; 58 | var v22 = v11 - v17; 59 | var v23 = v13 + v7; 60 | var v24 = v7 - v13; 61 | 62 | var v25 = v19 + v24; 63 | var v26 = v23 + v20; 64 | var v27 = v23 - v20; 65 | var v28 = v24 - v19; 66 | 67 | vector[0] = S[0] * v15; 68 | vector[1] = S[1] * v26; 69 | vector[2] = S[2] * v21; 70 | vector[3] = S[3] * v28; 71 | vector[4] = S[4] * v16; 72 | vector[5] = S[5] * v25; 73 | vector[6] = S[6] * v22; 74 | vector[7] = S[7] * v27; 75 | }; 76 | 77 | 78 | // DCT type III, scaled. A straightforward inverse of the forward algorithm. 79 | this.inverseTransform = function(vector) { 80 | var v15 = vector[0] / S[0]; 81 | var v26 = vector[1] / S[1]; 82 | var v21 = vector[2] / S[2]; 83 | var v28 = vector[3] / S[3]; 84 | var v16 = vector[4] / S[4]; 85 | var v25 = vector[5] / S[5]; 86 | var v22 = vector[6] / S[6]; 87 | var v27 = vector[7] / S[7]; 88 | 89 | var v19 = (v25 - v28) / 2; 90 | var v20 = (v26 - v27) / 2; 91 | var v23 = (v26 + v27) / 2; 92 | var v24 = (v25 + v28) / 2; 93 | 94 | var v7 = (v23 + v24) / 2; 95 | var v11 = (v21 + v22) / 2; 96 | var v13 = (v23 - v24) / 2; 97 | var v17 = (v21 - v22) / 2; 98 | 99 | var v8 = (v15 + v16) / 2; 100 | var v9 = (v15 - v16) / 2; 101 | 102 | var v18 = (v19 - v20) * A[5]; // Different from original 103 | var v12 = (v19 * A[4] - v18) / (A[2] * A[5] - A[2] * A[4] - A[4] * A[5]); 104 | var v14 = (v18 - v20 * A[2]) / (A[2] * A[5] - A[2] * A[4] - A[4] * A[5]); 105 | 106 | var v6 = v14 - v7; 107 | var v5 = v13 / A[3] - v6; 108 | var v4 = -v5 - v12; 109 | var v10 = v17 / A[1] - v11; 110 | 111 | var v0 = (v8 + v11) / 2; 112 | var v1 = (v9 + v10) / 2; 113 | var v2 = (v9 - v10) / 2; 114 | var v3 = (v8 - v11) / 2; 115 | 116 | vector[0] = (v0 + v7) / 2; 117 | vector[1] = (v1 + v6) / 2; 118 | vector[2] = (v2 + v5) / 2; 119 | vector[3] = (v3 + v4) / 2; 120 | vector[4] = (v3 - v4) / 2; 121 | vector[5] = (v2 - v5) / 2; 122 | vector[6] = (v1 - v6) / 2; 123 | vector[7] = (v0 - v7) / 2; 124 | }; 125 | 126 | 127 | var S = []; 128 | var C = []; 129 | for (var i = 0; i < 8; i++) { 130 | C.push(Math.cos(Math.PI / 16 * i)); 131 | S.push(1 / (4 * C[i])); 132 | } 133 | S[0] = 1 / (2 * Math.sqrt(2)); 134 | var A = [NaN, C[4], C[2] - C[6], C[4], C[6] + C[2], C[6]]; 135 | }; 136 | 137 | 138 | 139 | var fastDctLee = new function() { 140 | 141 | // DCT type II, unscaled. Algorithm by Byeong Gi Lee, 1984. 142 | // See: http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.118.3056&rep=rep1&type=pdf#page=34 143 | this.transform = function(vector) { 144 | var n = vector.length; 145 | if (n <= 0 || (n & (n - 1)) != 0) 146 | throw "Length must be power of 2"; 147 | transformInternal(vector, 0, n, new Float64Array(n)); 148 | }; 149 | 150 | 151 | function transformInternal(vector, off, len, temp) { 152 | if (len == 1) 153 | return; 154 | var halfLen = Math.floor(len / 2); 155 | for (var i = 0; i < halfLen; i++) { 156 | var x = vector[off + i]; 157 | var y = vector[off + len - 1 - i]; 158 | temp[off + i] = x + y; 159 | temp[off + i + halfLen] = (x - y) / (Math.cos((i + 0.5) * Math.PI / len) * 2); 160 | } 161 | transformInternal(temp, off, halfLen, vector); 162 | transformInternal(temp, off + halfLen, halfLen, vector); 163 | for (var i = 0; i < halfLen - 1; i++) { 164 | vector[off + i * 2 + 0] = temp[off + i]; 165 | vector[off + i * 2 + 1] = temp[off + i + halfLen] + temp[off + i + halfLen + 1]; 166 | } 167 | vector[off + len - 2] = temp[off + halfLen - 1]; 168 | vector[off + len - 1] = temp[off + len - 1]; 169 | } 170 | 171 | 172 | // DCT type III, unscaled. Algorithm by Byeong Gi Lee, 1984. 173 | // See: https://www.nayuki.io/res/fast-discrete-cosine-transform-algorithms/lee-new-algo-discrete-cosine-transform.pdf 174 | this.inverseTransform = function(vector) { 175 | var n = vector.length; 176 | if (n <= 0 || (n & (n - 1)) != 0) 177 | throw "Length must be power of 2"; 178 | vector[0] /= 2; 179 | inverseTransformInternal(vector, 0, n, new Float64Array(n)); 180 | }; 181 | 182 | 183 | function inverseTransformInternal(vector, off, len, temp) { 184 | if (len == 1) 185 | return; 186 | var halfLen = Math.floor(len / 2); 187 | temp[off + 0] = vector[off + 0]; 188 | temp[off + halfLen] = vector[off + 1]; 189 | for (var i = 1; i < halfLen; i++) { 190 | temp[off + i] = vector[off + i * 2]; 191 | temp[off + i + halfLen] = vector[off + i * 2 - 1] + vector[off + i * 2 + 1]; 192 | } 193 | inverseTransformInternal(temp, off, halfLen, vector); 194 | inverseTransformInternal(temp, off + halfLen, halfLen, vector); 195 | for (var i = 0; i < halfLen; i++) { 196 | var x = temp[off + i]; 197 | var y = temp[off + i + halfLen] / (Math.cos((i + 0.5) * Math.PI / len) * 2); 198 | vector[off + i] = x + y; 199 | vector[off + len - 1 - i] = x - y; 200 | } 201 | } 202 | 203 | }; 204 | 205 | 206 | 207 | var fastDctFft = new function() { 208 | 209 | // DCT type II, unscaled. 210 | this.transform = function(vector) { 211 | var len = vector.length; 212 | var halfLen = Math.floor(len / 2); 213 | var real = new Float64Array(len); 214 | for (var i = 0; i < halfLen; i++) { 215 | real[i] = vector[i * 2]; 216 | real[len - 1 - i] = vector[i * 2 + 1]; 217 | } 218 | if (len % 2 == 1) 219 | real[halfLen] = vector[len - 1]; 220 | for (var i = 0; i < len; i++) 221 | vector[i] = 0; 222 | transform(real, vector); 223 | for (var i = 0; i < len; i++) { 224 | var temp = i * Math.PI / (len * 2); 225 | vector[i] = real[i] * Math.cos(temp) + vector[i] * Math.sin(temp); 226 | } 227 | }; 228 | 229 | 230 | // DCT type III, unscaled. 231 | this.inverseTransform = function(vector) { 232 | var len = vector.length; 233 | if (len > 0) 234 | vector[0] /= 2; 235 | var real = new Float64Array(len); 236 | for (var i = 0; i < len; i++) { 237 | var temp = i * Math.PI / (len * 2); 238 | real[i] = vector[i] * Math.cos(temp); 239 | vector[i] *= -Math.sin(temp); 240 | } 241 | transform(real, vector); 242 | 243 | var halfLen = Math.floor(len / 2); 244 | for (var i = 0; i < halfLen; i++) { 245 | vector[i * 2 + 0] = real[i]; 246 | vector[i * 2 + 1] = real[len - 1 - i]; 247 | } 248 | if (len % 2 == 1) 249 | vector[len - 1] = real[halfLen]; 250 | }; 251 | 252 | }; 253 | 254 | module.exports = { 255 | fastDctFft, 256 | fastDctLee, 257 | fastDct8 258 | } -------------------------------------------------------------------------------- /data/DCT-circle.json: -------------------------------------------------------------------------------- 1 | { 2 | "content":"339 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \n337 4 -4 2 -1 1 0 0 4 -6 4 -2 2 -1 0 0 -3 4 -3 2 -1 1 0 0 2 -3 2 -1 1 0 0 0 -1 2 -1 1 0 0 0 0 1 -1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \n125 131 16 13 0 3 -1 -1 351 -102 -26 -9 -3 -1 1 1 -94 -32 33 3 5 -1 1 0 -1 69 -4 -4 -3 1 -1 -1 10 -24 -14 3 0 0 0 2 3 -3 10 1 1 -1 -1 -1 -2 2 -1 -5 0 1 1 0 0 1 -2 4 0 -1 -1 0 \n-53 36 3 1 1 0 0 0 441 10 1 0 0 0 0 0 67 -31 -3 0 -1 0 0 0 -68 -19 -1 0 0 0 0 0 -38 18 1 0 0 0 0 0 7 12 1 0 0 0 0 0 8 -3 0 0 0 0 0 0 2 -6 0 0 0 0 0 0 \n-43 -39 -1 -2 0 0 0 0 445 -11 0 0 0 0 0 0 54 34 1 2 0 0 0 0 -76 20 0 1 0 0 0 0 -31 -19 0 -1 0 0 0 0 14 -13 0 -1 0 0 0 0 7 3 0 0 0 0 0 0 -1 6 0 0 0 0 0 0 \n166 -141 19 -7 1 1 0 -1 297 137 -28 6 -2 0 0 2 -97 -6 40 -6 1 0 1 0 15 -48 -22 8 -1 1 -1 -1 9 32 -1 -6 1 -1 0 1 -2 -5 6 3 -2 1 0 -1 -2 -2 -4 1 3 -1 -1 0 1 1 1 -2 -2 0 1 0 \n339 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \n339 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \n337 4 -4 2 -1 1 0 0 4 -6 4 -2 2 -1 0 0 -3 4 -3 2 -1 1 0 0 2 -3 2 -1 1 0 0 0 -1 2 -1 1 0 0 0 0 1 -1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \n-63 244 49 4 8 0 1 0 244 143 -98 7 -16 1 -2 0 33 -98 -41 6 6 0 1 0 4 10 8 32 -4 0 -1 0 10 -20 6 -3 -7 0 0 0 0 2 0 0 -1 3 1 0 1 -1 1 -1 0 1 -2 0 0 0 0 0 0 0 0 1 \n29 -179 -22 -15 -3 -2 1 -1 -383 -20 36 6 5 1 1 0 -28 109 25 -1 1 -1 0 0 2 21 -43 -11 -2 -1 -1 1 4 -12 -9 7 3 1 1 1 8 -3 5 5 -1 -1 -1 0 1 -4 1 -3 -3 0 -1 0 1 0 2 -1 1 3 1 0 \n296 -55 -18 -3 0 0 0 0 -88 -75 -15 -3 1 0 0 0 -53 -44 -11 -1 1 1 0 0 -44 -35 -5 1 1 0 0 0 -25 -19 -1 1 1 0 0 0 -14 -7 0 1 1 0 0 0 -4 -2 1 1 1 0 0 0 -1 -1 0 0 0 0 0 0 \n278 70 -15 2 0 1 -1 0 -123 92 -12 1 0 1 -1 1 -70 49 -5 -2 1 1 -1 0 -54 33 3 -4 1 0 0 0 -27 12 5 -3 1 0 0 0 -13 1 4 -3 1 0 0 0 -3 -1 3 -2 1 0 0 0 -1 -1 1 -1 0 0 0 0 \n-24 195 -20 12 0 0 0 0 -374 -37 44 -6 4 0 0 0 4 -110 -8 8 -3 2 0 0 2 19 -46 -2 2 -1 1 0 11 11 7 -9 0 0 -1 1 5 4 5 3 -4 1 0 0 0 2 1 3 2 -2 1 -1 1 0 1 1 2 2 -2 1 \n6 -290 24 -16 3 -1 1 0 275 8 -98 3 -13 -1 -2 0 15 91 12 -28 5 -2 1 0 17 3 30 0 -12 1 -2 0 4 17 5 9 2 -3 1 -1 2 -1 2 1 3 1 -2 1 1 2 1 2 1 1 1 -1 0 0 0 0 0 0 0 0 \n339 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \n125 351 -141 -1 8 2 -2 0 131 -102 -32 52 -19 -2 2 1 11 -26 33 -3 -12 10 -1 -3 13 -12 4 -4 3 1 -5 7 0 -4 5 -2 0 1 0 0 5 -2 -1 1 1 -1 1 -1 -1 1 1 -1 0 -1 1 -1 -1 1 0 -1 1 -1 0 0 \n29 -383 -43 2 3 5 1 1 -179 -20 109 16 -9 -2 -4 0 -15 36 25 -34 -8 5 2 3 -15 8 -1 -11 8 4 -3 -2 -4 7 1 -2 3 -1 -3 1 -3 1 -1 -1 2 -1 0 3 1 1 0 -1 1 -1 -1 1 -1 0 0 0 1 0 0 0 \n339 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \n339 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \n339 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \n339 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \n-52 369 47 -19 12 1 -1 1 -172 -78 99 30 -6 0 4 -2 -11 -47 -17 24 17 -3 -1 2 -12 -18 -5 -6 8 5 1 -3 -1 -11 -1 -2 0 2 3 0 -1 -3 0 -2 1 0 1 3 1 -1 -1 -1 0 0 1 1 0 0 0 -1 0 1 1 0 \n206 -244 -154 -41 -2 2 0 -1 124 139 37 -11 -22 -7 -2 1 6 17 34 24 10 1 -3 -4 9 9 3 2 4 4 5 5 -2 -1 2 3 2 1 0 0 3 2 -1 -2 -2 -1 0 0 -2 -1 -1 0 0 -1 -1 -1 -1 -1 -1 -1 0 0 1 0 \n-53 441 100 -68 -31 5 8 2 36 10 -31 -14 14 7 -3 -9 2 1 -3 -1 1 1 0 -1 1 0 -1 0 0 0 0 0 1 0 -1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \n296 -88 -79 -44 -20 -9 -4 -2 -55 -75 -44 -26 -15 -4 -2 -1 -12 -15 -11 -4 -1 0 1 1 -3 -4 -1 1 1 1 1 1 0 1 1 1 1 1 1 0 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \n339 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \n339 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \n339 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \n339 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \n211 250 -194 83 -25 6 -1 0 -55 62 -13 -13 22 -10 8 -5 -12 12 -1 -5 4 -2 0 0 -3 3 2 -2 1 0 -1 1 0 -1 1 -1 0 1 -1 1 1 -1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \n32 -448 -66 82 20 -12 -5 4 36 10 -31 -14 14 7 -3 -9 2 1 -3 -1 1 1 0 -1 1 0 -1 0 0 0 0 0 1 0 -1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \n-43 445 81 -76 -25 9 7 -1 -39 -11 34 15 -15 -8 3 10 0 0 1 0 0 0 0 0 -2 -1 2 1 -1 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \n278 -123 -105 -54 -22 -8 -3 -1 70 92 49 25 10 1 -1 -1 -10 -12 -5 2 4 4 3 2 2 1 -2 -4 -3 -2 -2 -2 0 0 1 1 1 1 1 1 2 2 1 0 0 0 0 0 -1 -1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \n339 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \n339 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \n339 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \n339 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \n193 277 -196 70 -15 1 1 0 70 -73 9 22 -28 10 -5 2 -10 7 6 -9 4 1 -3 4 2 1 -4 4 0 -1 3 -3 0 0 1 -1 0 1 -1 1 2 -1 0 1 -1 0 0 0 -1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \n42 -444 -85 74 26 -8 -7 0 -39 -11 34 15 -15 -8 3 10 0 0 1 0 0 0 0 0 -2 -1 2 1 -1 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \n166 298 -146 15 7 -1 -2 2 -141 137 -6 -36 26 -3 -2 2 13 -28 40 -17 -1 6 -4 2 -7 9 -7 8 -6 2 1 -3 2 -2 1 -1 1 -2 3 -3 1 -1 0 1 -1 1 -1 0 0 0 1 -1 0 0 -1 1 -1 1 0 -1 1 -1 0 0 \n-24 -374 6 2 9 3 0 1 195 -37 -110 15 9 2 2 -1 -13 44 -8 -37 6 5 1 1 12 -8 10 -2 -10 3 3 1 0 5 -4 2 0 -3 2 3 -1 0 2 -2 1 1 -2 2 0 0 0 1 -1 0 1 -2 0 0 0 0 1 0 -1 1 \n339 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \n339 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \n339 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \n338 2 -2 1 -1 0 0 0 2 -2 2 -1 1 0 0 0 -1 2 -1 1 0 0 0 0 1 -1 1 -1 0 0 0 0 -1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \n-96 309 94 -24 14 1 2 0 169 158 -78 -30 -11 3 -5 1 1 -61 -34 -5 21 1 0 0 2 22 0 20 1 -2 -4 2 5 -8 0 -4 -2 -3 1 3 -2 1 0 2 1 2 1 1 0 0 1 0 -1 -1 -2 -2 0 0 0 0 0 1 1 0 \n239 -190 -134 -47 -8 1 2 1 -117 -143 -57 -11 8 5 4 2 0 6 20 19 12 6 2 1 2 5 5 3 0 -2 -3 -3 -3 -3 -2 -2 -2 -2 -2 -2 2 2 2 1 1 0 0 0 0 0 0 1 1 1 1 0 -1 -1 -1 0 0 0 0 0 \n339 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \n6 275 23 17 3 1 1 0 -290 8 91 2 13 0 2 0 16 -98 12 24 4 2 1 0 -16 3 -35 0 9 0 2 0 4 -16 5 -11 2 2 1 0 -2 -1 -2 1 -4 1 1 0 1 -2 1 -2 1 -2 1 1 0 0 0 0 0 1 -1 0 \n-52 -172 -16 -12 -1 -1 1 0 369 -78 -47 -14 -9 -2 -1 0 31 98 -17 -4 -1 0 -1 1 -19 40 30 -6 -2 -2 -1 -1 14 -8 20 8 0 1 0 0 1 1 -3 6 2 0 0 1 -1 3 -1 1 3 1 1 1 1 -1 1 -2 0 3 1 0 \n211 -55 -18 -3 0 0 0 0 250 62 12 2 -1 0 0 0 -129 -13 -1 1 1 0 0 0 83 -17 -7 -2 -1 0 0 0 -31 28 5 1 0 0 0 0 9 -18 -2 0 1 0 0 0 -1 7 0 -1 -1 0 0 0 0 -3 0 1 1 0 0 0 \n193 70 -15 2 0 1 -1 0 277 -73 7 0 0 -1 1 0 -131 9 6 -3 1 0 0 0 70 30 -11 4 -1 0 0 0 -18 -35 4 0 0 0 0 0 2 17 1 -2 1 0 0 0 1 -5 -3 2 -1 0 0 0 0 1 2 -2 1 0 0 0 \n-96 169 1 2 4 -2 0 0 309 158 -61 16 -6 0 0 0 63 -78 -34 0 0 0 1 0 -24 -40 -6 20 -5 1 0 -1 18 -14 23 1 -2 1 -1 0 1 5 1 -3 -4 2 -1 1 2 -5 0 -4 1 1 -2 1 0 1 0 1 2 1 -2 0 \n79 -279 0 -10 -4 0 -1 0 -279 -116 71 5 1 2 -1 1 0 71 62 -16 -3 1 -1 1 -10 6 -20 -24 2 1 -1 1 -5 2 -3 2 7 1 -1 1 1 4 1 1 1 -2 -1 1 -1 -1 -1 -1 -1 -1 1 1 0 1 0 0 1 0 1 0 \n339 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \n339 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \n339 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \n206 124 10 9 -2 2 -2 -1 -244 139 17 7 -1 1 -2 -2 -103 37 34 2 2 -1 -1 -2 -41 -15 30 2 3 -1 0 -1 -3 -27 12 3 2 -1 0 0 3 -11 1 5 1 -1 -1 0 0 -2 -3 5 0 0 -1 1 -1 1 -2 3 0 0 -1 0 \n32 36 3 1 1 0 0 0 -448 10 1 0 0 0 0 0 -44 -31 -3 0 -1 0 0 0 82 -19 -1 0 0 0 0 0 25 18 1 0 0 0 0 0 -20 12 1 0 0 0 0 0 -5 -3 0 0 0 0 0 0 3 -6 0 0 0 0 0 0 \n42 -39 -1 -2 0 0 0 0 -444 -11 0 0 0 0 0 0 -57 34 1 2 0 0 0 0 74 20 0 1 0 0 0 0 33 -19 0 -1 0 0 0 0 -13 -13 0 -1 0 0 0 0 -7 3 0 0 0 0 0 0 0 6 0 0 0 0 0 0 \n239 -117 -1 2 -2 1 0 -1 -190 -143 6 3 -3 1 0 -2 -90 -57 20 4 -2 2 0 -2 -47 -15 23 3 -2 1 1 -1 -10 10 14 0 -2 1 1 0 2 9 6 -2 -3 0 1 0 2 4 2 -3 -2 0 1 0 1 1 0 -2 -1 0 0 0 \n339 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \n339 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0" 3 | } -------------------------------------------------------------------------------- /data/DCT-eye.json: -------------------------------------------------------------------------------- 1 | { 2 | "content": "-126 -72 -11 4 0 0 -1 1 -16 -33 16 8 0 -1 1 -2 -2 18 0 5 -1 1 1 0 -8 3 -2 -2 0 0 0 0 2 11 2 1 0 0 0 0 -7 1 1 -1 0 0 0 0 1 -1 -1 0 0 0 0 0 0 0 0 -1 -1 1 0 0 \n-115 15 15 2 1 -2 0 -1 209 -34 -9 -6 -3 -2 0 0 -9 -7 -1 -2 -2 0 0 1 -24 18 5 -2 0 0 0 0 3 1 3 -2 1 0 0 0 -1 -2 3 -2 0 0 0 0 -1 -1 1 0 0 0 0 0 -1 -1 0 0 -1 0 0 1 \n-13 -95 22 -7 0 1 0 0 251 41 -20 6 -2 1 -1 0 -55 32 5 -1 2 -1 0 0 -9 -36 11 2 -2 0 0 0 6 11 -6 2 0 0 0 0 0 0 2 0 1 0 0 0 -2 -1 -1 1 -1 0 1 0 0 0 -1 -1 -1 1 1 0 \n39 66 28 -3 -2 -1 2 -1 -94 56 9 7 5 0 1 0 -13 -8 -6 -10 -1 1 0 -1 9 8 8 2 -1 1 0 0 -3 -5 -2 -1 1 0 0 0 1 1 2 -1 0 0 1 0 -1 -1 0 0 0 -1 0 1 0 0 0 0 1 0 0 0 \n96 -30 -18 -7 2 1 -2 0 23 -27 -4 -2 0 -2 1 0 12 12 8 3 -1 1 -2 -3 -4 -7 -2 1 0 1 0 -1 -3 -2 -1 0 2 -1 1 1 -1 0 0 1 -1 0 -1 0 0 -1 -1 -1 0 0 0 0 0 -1 0 0 0 0 0 0 \n115 -10 -13 -3 5 1 1 1 -7 -29 -9 1 2 2 2 0 0 -12 -10 0 1 1 -1 3 -9 3 5 0 2 0 0 0 1 2 0 0 -2 1 0 0 2 1 -1 0 0 0 0 1 2 2 1 0 0 -1 0 0 1 0 0 0 0 0 0 0 \n65 5 46 0 8 -3 -1 2 -50 49 7 -8 -5 0 2 2 -11 13 -2 0 -1 0 2 3 -1 7 1 5 2 1 1 0 -2 0 -1 0 1 0 0 0 -3 0 1 0 -1 -1 -1 1 -1 1 0 0 0 0 0 0 0 0 1 0 0 1 1 0 \n136 -38 9 10 6 -3 1 -1 -72 -29 12 10 7 -2 0 -1 -12 -5 3 4 2 -1 0 -2 -8 -1 1 3 1 -1 0 -1 -4 0 1 0 -1 -1 0 -1 0 0 1 1 0 0 0 0 1 1 1 0 0 0 0 1 0 0 0 0 0 -1 0 0 \n-167 32 -53 16 -4 -2 -1 0 -5 -12 2 -5 2 1 0 -1 20 10 0 2 1 0 0 0 -21 -17 -2 3 1 0 -1 0 -8 -9 1 1 1 0 0 0 -2 2 1 0 0 0 0 0 1 1 0 0 0 0 0 0 1 0 0 1 0 0 0 0 \n-184 -96 -20 1 3 1 0 0 -114 40 35 4 -1 -1 0 0 -10 28 -8 -8 0 0 0 1 1 2 -10 2 1 0 0 -1 -1 2 -2 1 0 0 0 0 0 -1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 \n-171 47 19 -5 -1 1 -1 -1 -29 -7 -29 6 -3 0 0 0 -60 -5 13 -2 1 -1 1 0 2 0 -4 -2 -1 1 0 -1 -7 -4 -2 1 1 0 0 1 -5 0 1 -1 0 0 0 0 0 -1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \n-130 -125 58 -13 6 -2 1 1 84 -101 -40 14 -3 0 0 0 21 22 -40 -7 4 -2 1 0 12 7 -2 -8 0 1 -1 1 5 5 -2 -2 -2 0 0 0 2 2 0 -1 -1 0 0 0 1 1 0 0 -1 0 0 0 0 0 0 0 0 0 0 0 \n96 65 -14 -13 -2 0 0 -1 -2 -3 21 -2 1 0 1 0 4 -13 -4 2 -4 0 0 0 -3 4 -2 0 1 1 1 1 -3 3 -1 -1 0 0 0 -1 -1 0 1 0 -1 0 0 -1 0 1 -1 0 0 0 0 0 0 0 0 0 0 0 0 1 \n103 -28 -20 5 1 1 -2 2 17 19 2 4 -1 -2 0 2 2 -5 -5 3 0 0 -1 -1 0 5 3 0 -3 0 -1 2 5 1 -1 -3 0 1 0 0 -1 -2 1 1 -1 0 0 1 0 0 0 -1 0 0 0 1 0 -1 0 -1 0 -1 0 0 \n130 -51 12 8 1 2 2 -2 -17 -15 1 -4 -2 1 1 -1 -2 0 1 -2 1 2 -1 -1 -3 -3 -1 -2 0 0 -1 -3 -2 0 0 -2 -1 1 0 0 1 1 0 0 -2 0 0 0 0 0 -1 0 0 -1 -1 0 -1 0 0 0 0 0 0 0 \n163 11 3 -4 4 -3 -1 -2 8 -6 -6 3 -2 0 -1 -1 1 -3 0 -2 -1 1 -1 1 3 2 -2 -3 0 0 1 0 3 -3 2 -1 -1 0 0 -1 0 0 0 0 0 0 0 -1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \n-64 218 -70 1 10 -4 0 1 -41 -33 10 5 2 1 1 0 5 -18 -8 6 -2 -1 1 -1 -6 -6 1 0 0 0 0 0 -3 -7 -1 0 -1 0 0 0 -2 -1 1 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \n-79 -7 -95 -39 -20 -4 -1 0 -2 -9 7 7 2 1 0 -1 0 -16 -7 -2 2 2 2 1 2 1 3 -1 -1 -1 0 0 -2 -2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \n-237 -70 103 -14 16 -1 2 0 -18 10 2 -4 6 -1 1 0 20 3 3 8 -3 1 -1 0 -24 28 -12 3 -1 0 0 0 -4 12 -6 4 -2 0 0 0 1 -3 2 -2 0 0 0 0 1 -1 0 0 0 0 0 0 1 -1 1 0 0 1 -1 0 \n-133 169 46 -31 -4 -5 5 1 44 48 -3 -16 -13 -1 2 3 -21 -23 -3 -2 2 -1 -1 0 -22 -20 -1 5 2 -1 -2 -2 -18 -20 -3 1 1 0 0 0 -4 -2 1 2 2 1 0 0 0 0 1 0 0 0 0 0 -2 -2 -1 0 0 0 0 -1 \n82 57 -84 -17 -6 -5 -2 -1 7 -15 -7 0 -3 -2 1 -2 -3 10 7 0 2 -1 0 0 6 -3 -2 2 -1 -1 0 0 -4 4 -1 1 0 0 0 0 1 2 0 1 -1 1 0 0 0 0 0 0 0 0 0 0 -1 0 0 -1 0 0 0 0 \n53 -63 -33 -6 -2 1 0 1 15 -6 -5 10 -1 1 1 -1 8 1 3 -5 0 0 0 1 4 0 1 -2 1 1 1 0 -3 -3 1 -1 1 0 0 0 0 -2 0 0 0 0 0 1 -2 -1 0 -1 -1 0 0 -1 1 1 1 0 0 1 0 0 \n124 -9 -10 -7 -2 1 -1 0 7 -21 0 1 5 0 0 -1 4 -4 -3 0 -1 0 1 -1 0 -3 1 -1 2 -1 0 0 0 3 2 1 1 0 0 1 2 0 -2 0 1 0 0 0 1 1 1 0 0 0 0 0 1 1 0 0 0 0 0 0 \n148 -33 36 -7 -1 2 0 2 10 29 7 -9 3 1 0 0 -6 -4 -6 -1 2 2 0 -1 -2 2 -1 -2 0 0 0 -1 2 1 -1 0 0 0 0 0 0 -1 -1 0 -1 -1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \n65 31 -75 15 -2 3 -1 0 -30 99 -42 6 -2 0 1 1 -14 14 10 -1 2 -1 1 0 1 -2 0 -2 1 0 1 0 -4 1 1 -1 -1 0 0 1 2 0 -1 1 0 0 0 0 -1 0 1 -1 0 0 0 0 0 -1 0 1 1 0 0 0 \n-121 -18 -10 6 5 1 1 0 -6 -131 -48 -4 3 2 0 0 53 49 -36 -13 -3 2 0 0 -30 12 24 -7 -2 0 0 0 12 -9 -2 1 -1 0 1 1 -6 5 1 0 0 -1 0 0 2 -1 0 0 0 0 0 0 -1 1 0 0 0 0 0 0 \n-137 17 -8 10 0 0 0 1 52 -4 7 2 -3 -1 0 1 -19 -29 17 2 -2 -1 0 0 -12 12 5 1 -2 0 0 1 2 -8 5 2 0 0 0 0 -8 2 1 0 0 0 0 0 1 -1 1 0 0 0 0 0 -1 0 0 0 0 0 0 0 \n-216 -47 94 -32 14 -4 1 0 -2 38 -9 -5 6 -2 2 -1 12 16 -4 4 0 0 1 -1 -6 -7 -4 0 1 -1 0 0 2 6 0 1 -1 1 -1 0 0 -3 -1 0 0 0 0 0 0 0 0 0 0 0 0 0 -1 0 -1 0 0 0 0 0 \n-21 64 24 -10 0 0 0 -2 92 43 -25 -14 -2 0 -1 -1 -19 -13 -13 0 2 0 1 1 7 11 -1 4 -1 0 0 1 3 -1 -2 0 0 0 0 0 -1 1 1 0 1 0 0 0 0 0 -1 1 0 0 0 0 0 -1 1 0 0 0 0 0 \n19 -68 37 -8 -4 0 0 0 28 7 3 -11 1 1 -2 2 -2 12 -18 -2 1 1 0 -1 -1 -11 -4 -1 1 -1 1 0 -3 -2 -5 2 1 0 0 0 3 -2 -2 -1 0 0 0 0 -1 -1 0 0 0 0 0 -1 0 0 0 0 -1 0 0 0 \n114 12 1 -4 -2 1 2 0 25 7 0 2 -1 0 0 0 -13 -5 0 -1 1 0 0 -1 2 1 -1 -1 -1 -1 1 0 -1 3 0 1 0 0 0 1 2 -1 -2 1 -1 0 0 -1 -1 1 0 0 1 0 1 0 1 1 0 -1 0 0 0 0 \n132 -40 5 11 0 0 1 1 13 -10 6 -1 2 0 1 1 -6 -2 0 -1 -1 0 0 0 -1 -2 1 0 0 0 0 0 1 0 1 0 -1 0 0 0 3 0 -1 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 \n-36 -71 -10 2 -5 -3 1 0 39 -6 13 1 1 -2 -2 1 -11 -15 10 0 0 0 0 -1 -1 -16 7 3 1 0 0 0 5 -5 -1 5 0 0 1 0 0 1 1 0 0 -1 0 0 -2 -1 -2 -1 -1 1 0 0 2 0 0 -1 1 -1 0 0 \n72 -40 -10 -6 1 0 1 0 91 12 -3 5 1 -1 -1 1 6 25 -7 -3 -1 2 0 -1 -6 13 -2 0 0 0 1 0 -11 2 3 0 -1 -1 0 0 2 -1 0 2 1 0 -1 -1 0 1 0 0 -1 0 0 0 1 1 1 0 0 0 0 0 \n45 144 -55 7 -1 0 0 -1 80 -42 -6 17 -7 1 0 1 -33 -30 25 -4 0 2 0 -1 -8 -6 -3 -2 1 0 0 0 0 -5 4 0 0 0 0 0 -2 0 0 0 1 0 0 0 0 -1 0 0 0 0 0 0 -1 0 0 1 -1 0 0 0 \n-193 21 13 -1 0 -1 1 0 -4 -42 -5 14 0 3 0 1 -7 -6 40 8 -1 0 0 -1 4 11 12 -2 -4 0 0 -1 1 2 0 0 1 1 0 1 5 3 2 -1 0 0 0 0 -1 -1 0 -1 0 0 0 0 1 1 0 0 0 0 0 0 \n-100 -49 -13 -5 -2 -2 1 0 -28 -31 -4 12 3 0 -1 1 -10 1 16 3 -2 3 -1 -1 -3 13 1 1 0 1 -1 2 0 1 1 0 0 -1 0 1 0 0 2 -1 -1 0 0 0 -1 0 0 0 0 0 1 0 0 0 -1 0 0 0 0 0 \n-18 -10 -22 1 0 1 0 -2 11 -10 -2 -1 -2 1 -1 -2 1 -5 -4 0 2 0 0 0 1 -2 1 2 0 1 0 0 -6 1 -1 1 0 0 1 0 0 1 1 1 0 0 0 0 0 1 1 0 0 0 -1 1 1 0 0 0 0 0 0 1 \n-24 9 -9 -12 1 0 0 1 40 -32 -1 6 2 0 -1 0 13 1 3 2 -1 0 0 -1 3 2 2 -1 -2 0 1 0 -5 -1 -1 1 -1 0 0 0 1 1 -2 -1 1 0 -1 1 0 0 1 0 1 0 0 -1 1 0 1 0 1 0 0 0 \n-12 35 -37 11 -5 3 0 1 115 -29 1 0 -4 0 1 -1 1 1 1 2 2 -1 -1 0 4 -7 4 -2 1 0 0 -1 1 -2 1 0 -1 0 0 0 3 0 1 0 0 0 0 0 0 0 0 1 1 0 0 0 1 0 0 0 0 0 0 0 \n-135 -46 2 1 1 -2 0 0 30 5 -2 -3 -1 -1 0 -1 25 13 -2 -3 2 -1 -1 0 15 7 2 -2 -1 0 0 -1 -3 -3 4 0 0 -1 1 0 -6 -1 0 -1 1 0 0 0 4 -3 2 0 -1 0 0 0 2 0 0 0 0 -1 0 0 \n-12 12 -19 -6 -5 -3 -2 -1 13 -14 -13 10 2 1 0 0 10 -14 10 10 -2 1 0 -1 5 1 7 -4 2 0 0 -1 0 1 -5 0 1 0 0 0 -3 1 0 1 -2 -1 0 0 0 0 -1 1 0 0 0 0 2 0 0 1 0 0 0 0 \n-92 60 0 -5 -2 -1 0 2 -5 1 9 -4 -3 1 -1 0 4 1 17 -2 2 0 0 -1 9 9 9 4 -1 -1 -1 0 8 6 5 -2 0 0 0 1 0 0 1 0 0 0 0 0 2 -1 0 -1 0 -1 0 -1 0 0 0 -1 0 0 1 0 \n-212 26 45 -9 2 0 0 0 7 -23 5 -6 3 -2 1 1 -3 7 9 1 0 -1 1 0 -2 -1 -2 -3 0 0 0 0 1 2 -1 -1 0 0 0 0 0 2 -1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 \n-123 -35 -13 6 -2 -2 -1 0 68 5 3 0 0 0 0 -1 -11 -6 7 3 -1 -1 0 -1 9 -4 -2 0 1 0 0 -1 -3 1 2 -1 1 1 0 0 1 1 1 1 -1 0 0 0 0 -1 -1 0 0 0 0 0 0 -1 -1 -1 0 0 0 0 \n-82 -3 -13 -6 2 0 0 0 70 -6 1 -2 1 0 1 0 -9 5 2 -2 0 -1 0 -1 6 -2 -3 0 0 0 0 0 -6 2 -2 0 1 0 0 0 -1 2 0 0 0 0 0 0 0 0 0 0 -1 0 0 0 0 0 0 0 0 0 0 0 \n-96 20 4 -6 -1 2 0 0 29 28 2 -3 2 1 0 0 -3 -6 1 3 1 0 1 -1 6 1 0 2 -1 0 0 0 -1 -1 -2 1 0 0 0 1 -1 1 -1 0 0 0 0 -1 -1 -1 0 0 -1 0 0 0 1 0 1 0 0 0 0 0 \n-166 25 -11 3 0 0 0 2 20 20 -18 9 -10 -2 0 0 10 6 -11 2 5 -1 -1 0 -9 -2 -3 -6 2 1 0 1 1 2 2 -1 -2 0 0 0 2 0 0 1 1 0 0 0 0 -1 0 0 0 0 0 0 0 0 0 0 0 1 0 0 \n-139 -80 -11 7 3 -1 1 0 31 -44 5 -2 1 0 1 0 -27 26 11 0 -1 -1 0 1 3 5 1 4 1 0 -1 1 -1 6 -1 0 1 0 0 0 1 -1 3 0 0 -1 0 0 2 1 -1 -1 0 0 0 0 0 0 0 -1 0 0 0 0 \n-110 -16 4 6 1 3 1 1 81 25 4 -3 1 -1 0 0 -1 7 1 -7 -5 0 1 0 7 -3 -6 0 1 0 0 0 -5 -3 -2 0 -1 0 0 0 2 1 1 2 0 0 0 0 -2 -1 1 0 0 0 0 0 1 -2 1 0 0 1 0 0 \n-2 -108 19 -4 -7 -5 0 -1 -19 55 6 4 5 1 -1 -2 -30 -1 17 2 -4 -1 0 -1 4 13 -6 -4 1 0 0 0 -5 -2 1 -1 0 -1 0 0 0 1 -2 -1 0 0 0 0 2 2 0 1 1 0 0 0 1 0 1 1 0 1 0 0 \n-66 175 14 11 -6 0 -1 0 -220 -5 47 3 2 -2 0 -1 33 -44 -5 10 0 1 -1 0 3 21 -12 -2 1 0 0 -1 0 -1 2 -2 0 0 0 0 0 -1 1 1 0 1 0 0 -1 -1 0 0 0 0 0 0 1 0 -1 0 0 0 0 0 \n-242 -34 -13 -7 4 1 -1 1 -28 6 -2 8 -5 -2 0 0 -10 2 6 0 2 2 1 0 2 -5 -3 -4 0 0 0 0 0 2 0 1 0 0 0 0 3 0 0 -1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \n-239 13 6 7 1 1 0 0 44 -11 -20 -3 -2 0 0 0 -4 -14 -1 3 1 0 0 0 -11 17 9 0 0 0 0 0 5 0 -1 -1 0 0 0 0 -1 0 -1 0 1 0 0 0 1 -1 -1 0 0 0 0 0 0 0 0 0 0 0 0 0 \n-181 -92 31 -5 -3 0 1 0 -16 22 -7 1 0 1 -1 0 38 8 -15 -1 -2 0 0 0 -18 0 4 -1 0 0 0 0 14 2 -1 -1 0 0 0 0 -3 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \n-93 36 -3 3 2 -1 1 0 -51 -14 1 -2 3 -1 0 0 1 5 0 -3 -1 -1 0 0 -10 2 -1 0 0 0 0 0 0 -2 1 1 0 0 0 0 -4 0 1 -1 0 1 0 0 1 -1 0 0 0 0 0 0 1 0 0 0 0 0 0 0 \n-104 -37 -19 -2 0 0 0 0 49 -9 3 -1 1 0 0 0 -16 3 0 0 0 1 0 0 -15 9 -1 1 0 0 0 0 12 -5 0 0 0 0 0 0 0 6 -2 0 0 0 0 0 6 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \n-87 5 16 5 -8 0 0 -1 -28 4 13 -1 2 1 1 0 -32 10 6 1 -3 -1 0 0 -21 3 -2 2 -1 0 -1 1 9 7 2 -1 -1 1 0 0 -3 -4 2 0 0 0 0 0 2 2 0 -1 1 0 0 0 0 0 0 0 0 0 0 0 \n38 -104 2 -21 -8 -4 0 -1 0 7 5 4 -1 -2 1 0 15 -30 -7 2 -1 1 -1 2 -27 -14 4 -6 -4 -1 0 0 -2 4 0 0 0 0 0 -1 -3 -2 2 0 0 0 1 0 0 -1 1 1 1 0 0 0 0 0 0 0 -1 0 0 0 \n201 -3 -19 6 -4 1 -1 0 -5 14 -3 3 -4 0 -1 0 -2 9 -6 2 0 1 0 1 -1 -1 -3 0 0 0 0 0 -2 1 -1 -1 0 0 0 0 1 2 1 0 0 0 0 0 0 0 0 0 0 0 0 0 -1 0 0 0 0 0 0 0 \n-190 117 103 15 14 3 1 1 -102 -67 -38 8 3 3 1 2 19 -18 -4 -16 -3 0 0 -1 0 10 -1 3 -1 -2 -1 1 4 -2 0 0 0 1 0 -1 -1 -1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 \n45 -167 -42 -2 -1 -1 -1 1 -192 -57 16 11 4 1 0 1 -21 28 21 7 1 0 -1 0 -11 13 1 -3 -2 0 0 0 -8 2 -3 -1 -1 0 0 0 -1 3 0 1 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \n119 36 -2 3 -1 0 0 0 -28 -28 -10 3 0 -1 1 -1 -30 -2 0 0 -1 1 0 0 -8 -12 0 -2 0 0 0 0 -5 -6 -1 0 -1 0 0 0 -3 -1 0 -1 0 0 0 0 -1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \n-35 98 -15 2 3 -1 1 -1 40 -37 7 7 -3 0 0 0 -8 7 -8 -5 0 0 -1 1 -12 6 2 -1 1 0 1 0 6 -6 0 2 0 0 0 -1 -1 2 0 0 0 0 0 0 0 -2 0 0 0 0 0 0 0 0 0 0 0 0 0 0" 3 | } -------------------------------------------------------------------------------- /components/utils/ImageUtilities.js: -------------------------------------------------------------------------------- 1 | import 'whatwg-fetch'; 2 | var ace = require('brace'); 3 | var {fastDctLee} = require('./FastDct'); 4 | require('brace/mode/assembly_x86'); 5 | require('brace/theme/monokai'); 6 | require('brace/ext/searchbox'); 7 | 8 | 9 | // Polyfill for browsers besides chrome and firefox, 10 | // taken from https://dev.to/nektro/createimagebitmap-polyfill-for-safari-and-edge-228 11 | if (typeof window !== 'undefined' && !('createImageBitmap' in window)) { 12 | window.createImageBitmap = function(blob) { 13 | return new Promise((resolve, reject) => { 14 | let img = document.createElement('img'); 15 | img.addEventListener('load', function() { 16 | resolve(this); 17 | }); 18 | img.src = URL.createObjectURL(blob); 19 | }); 20 | } 21 | } 22 | 23 | function pad(array) { 24 | // Pad an array with 0's to reach a power of 2 length. 25 | let nextPowerOf2 = 2 ** Math.ceil(Math.log2(array.length)); 26 | for (let i = array.length; i < nextPowerOf2; i++) { 27 | array.push(0); 28 | } 29 | return array; 30 | } 31 | 32 | // Extracts the R, G, or B component from image data. 33 | function extractComponentFromPixelData(pixelData, componentName) { 34 | let index = 0; 35 | if (componentName == 'G') index = 1; 36 | if (componentName == 'B') index = 2; 37 | 38 | let values = []; 39 | 40 | for (let i = 0; i < pixelData.length; i+= 4) { 41 | values.push(pixelData[i + index]); 42 | } 43 | 44 | return values; 45 | } 46 | 47 | // Fills the given image data's component with new values 48 | function fillPixelData(pixelData, componentName, values) { 49 | let index = 0; 50 | if (componentName == 'G') index = 1; 51 | if (componentName == 'B') index = 2; 52 | 53 | let count = 0; 54 | 55 | for (let i = 0; i < pixelData.length; i+= 4) { 56 | pixelData[i + index] = values[count]; 57 | count ++; 58 | } 59 | } 60 | 61 | function clampTo8bit(a) { 62 | return a < 0 ? 0 : a > 255 ? 255 : a; 63 | } 64 | 65 | let corruptedCache = {}; 66 | 67 | let componentMap = { 68 | 'Y' : 0, 69 | 'Cb' : 1, 70 | 'Cr' : 2 71 | } 72 | 73 | function subsampleComponent(component, scale, width, height) { 74 | let biggerDimension = width > height ? width : height; 75 | if (scale > biggerDimension) scale = biggerDimension; 76 | 77 | for (let y = 0; y < height; y+= scale) { 78 | for (let x = 0; x < width; x+= scale) { 79 | // Compute the average of this block of pixels 80 | let sum = 0; 81 | let count = 0; 82 | for (let y_inner = 0; y_inner < scale; y_inner ++) { 83 | for (let x_inner = 0; x_inner < scale; x_inner++) { 84 | let finalX = x + x_inner; 85 | let finalY = y + y_inner; 86 | 87 | if (finalX > width - 1) finalX = width - 1; 88 | if (finalY > height - 1) finalY = height - 1; 89 | 90 | sum += component[0 | (finalY * width + finalX)]; 91 | count ++; 92 | } 93 | } 94 | // Set all the pixels to this average 95 | let avg = sum / count; 96 | for (let y_inner = 0; y_inner < scale; y_inner ++) { 97 | for (let x_inner = 0; x_inner < scale; x_inner++) { 98 | let finalX = x + x_inner; 99 | let finalY = y + y_inner; 100 | 101 | if (finalX > width - 1) finalX = width - 1; 102 | if (finalY > height - 1) finalY = height - 1; 103 | 104 | component[0 | (finalY * width + finalX)] = avg; 105 | } 106 | } 107 | } 108 | } 109 | 110 | return component; 111 | } 112 | 113 | function getHeaderAndBody(buffer) { 114 | let data = { 115 | header : [], 116 | body: [] 117 | }; 118 | 119 | let bytes = new Uint8Array(buffer); 120 | let isBody = false; 121 | for (let i = 0; i < bytes.length; ++i) { 122 | // Start is FFDA (255 218) 123 | // End is FFD9 (255 217) 124 | // If there exists a thumbnail, it will trip this up 125 | // Maybe find a way to check for a thumbnail and ignore it? 126 | if (i < bytes.length - 1 && bytes[i] == 255 && bytes[i+1] == 218) { 127 | isBody = true; 128 | } 129 | 130 | if (isBody) { 131 | data.body.push(bytes[i]); 132 | } else { 133 | data.header.push(bytes[i]); 134 | } 135 | } 136 | 137 | return data; 138 | } 139 | 140 | function resizeImage(url, maxWidth, callback) { 141 | var img = new Image(); 142 | img.crossOrigin = "Anonymous"; 143 | 144 | img.onload = function () { 145 | var canvas = document.createElement("canvas"); 146 | var ctx = canvas.getContext("2d"); 147 | // set size proportional to image 148 | let percent = 1; 149 | 150 | if (img.width > maxWidth) { 151 | percent = maxWidth / img.width; 152 | } 153 | 154 | canvas.width = img.width * percent; 155 | canvas.height = img.height * percent; 156 | 157 | ctx.drawImage(img, 0, 0, canvas.width, canvas.height); 158 | 159 | callback(canvas); 160 | } 161 | img.src = url; 162 | } 163 | 164 | class ImageUtilities { 165 | constructor(options) { 166 | let url = options.url; 167 | let that = this; 168 | this.editMode = options.editMode; 169 | this.corruptedImage = options.corruptedImage; 170 | this.imageWidth = 0; 171 | this.imageHeight = 0; 172 | this.showHeader = options.showHeader; 173 | 174 | // This article allows you to override the given image with a query param 175 | // unless specifically instructed not to by passing the `options.isUrlExempt`. 176 | let hasImageSrc = window.location.search.indexOf("?imageSrc") != -1; 177 | let loadFromUrl = false; 178 | if (hasImageSrc && options.isUrlExempt != true) { 179 | loadFromUrl = true; 180 | let maxWidth = 400; 181 | if (options.maxWidth != undefined) { 182 | maxWidth = options.maxWidth; 183 | } 184 | let imageSrc = window.location.search.replace("?imageSrc=",""); 185 | resizeImage(imageSrc, maxWidth, function(canvas) { 186 | canvas.toBlob(function(blob) { 187 | return loadImageFromResponse(new Response(blob)) 188 | .then(function() { 189 | options.callback(that); 190 | }) 191 | }, 'image/jpeg', 0.95); 192 | }) 193 | } 194 | 195 | this.savedComponents = {}; 196 | 197 | if (this.showHeader == undefined) { 198 | this.showHeader = true; 199 | } 200 | 201 | if (this.corruptedImage == undefined) { 202 | this.corruptedImage = 'corrupted.png'; 203 | } 204 | // Load the corrupted image from the cache if it exists. 205 | if (corruptedCache[this.corruptedImage]) { 206 | this.corruptedImage = corruptedCache[this.corruptedImage]; 207 | } else { 208 | fetch('static/images/' + this.corruptedImage) 209 | .then(function(response) { 210 | return response.blob(); 211 | }) 212 | .then(function(blob) { 213 | return createImageBitmap(blob); 214 | }) 215 | .then(function(imageBitmap) { 216 | corruptedCache[that.corruptedImage] = imageBitmap; 217 | that.corruptedImage = imageBitmap; 218 | }) 219 | .catch(function(error) { 220 | console.log("Error creating corrupted image", error); 221 | }) 222 | } 223 | 224 | this.highlightPixelOnClick = options.highlightPixelOnClick; 225 | 226 | function loadImageFromResponse(response) { 227 | return response.arrayBuffer() 228 | .then(function(buffer) { 229 | if (that.editMode == 'raw') { 230 | let data = getHeaderAndBody(buffer); 231 | that.body = data.body; 232 | that.header = data.header; 233 | if (that.showHeader) { 234 | that.totalBytes = that.header.concat(that.body); 235 | } else { 236 | that.totalBytes = that.body; 237 | } 238 | 239 | } else { 240 | that.decodedImage = jpeg.decode(buffer, { useTArray:true }); 241 | that.imageWidth = that.decodedImage.width; 242 | that.imageHeight = that.decodedImage.height; 243 | } 244 | }) 245 | } 246 | 247 | if (!loadFromUrl) { 248 | fetch(url) 249 | .then(function(response) { 250 | return loadImageFromResponse(response); 251 | }).then(function() { 252 | options.callback(that); 253 | }) 254 | .catch(function(error) { 255 | console.log(error) 256 | }); 257 | } 258 | 259 | } 260 | 261 | fillDecodedComponent(componentName, values) { 262 | let component = this.decodedImage._decoder.components[componentMap[componentName]]; 263 | let index = 0; 264 | for (let i = 0; i < component.lines.length; i++) { 265 | let line = component.lines[i]; 266 | for (let j = 0; j < line.length; j++) { 267 | line[j] = values[index]; 268 | index ++; 269 | } 270 | } 271 | } 272 | 273 | subsampleAndRedraw(componentName, scale) { 274 | if (componentName == 'Y' || componentName == 'Cb' || componentName == 'Cr') { 275 | let component = this.savedComponents[componentName]; 276 | if (component == undefined) { 277 | component = this.getDecodedComponent(componentName); 278 | this.savedComponents[componentName] = component.slice(); 279 | } 280 | component = component.slice(); 281 | 282 | if (scale == 0) { 283 | for (let i = 0; i < component.length; i++) { 284 | let defaultVal = 128; 285 | 286 | component[i] = defaultVal; 287 | } 288 | } else { 289 | // TODO make sure to pass the right width and height here if the component is downsampled already? 290 | component = subsampleComponent(component, scale, this.decodedImage.width, this.decodedImage.height); 291 | } 292 | 293 | this.fillDecodedComponent(componentName, component); 294 | 295 | let decodedImage = this.decodedImage; 296 | decodedImage._decoder.copyToImageData({ 297 | width: decodedImage.width, 298 | height: decodedImage.height, 299 | data: decodedImage.data 300 | }); 301 | 302 | this.drawDecodedImage(); 303 | } else if (componentName == 'R' || componentName == 'G' || componentName == 'B') { 304 | let decodedImage = this.decodedImage; 305 | let dataCopy = decodedImage.data.slice(); 306 | let component = extractComponentFromPixelData(dataCopy, componentName); 307 | 308 | if (scale == 0) { 309 | for (let i = 0; i < component.length; i++) { 310 | component[i] = 0; 311 | } 312 | } else { 313 | component = subsampleComponent(component, scale, decodedImage.width, decodedImage.height); 314 | } 315 | 316 | fillPixelData(dataCopy, componentName, component); 317 | this.drawPixelData(dataCopy, decodedImage.width, decodedImage.height); 318 | } 319 | } 320 | 321 | onEditorChange() { 322 | if (this.editMode == 'raw') { 323 | // Update the body, since it's the only thing in the editor right now 324 | let values = this.getValuesFromEditor(); 325 | this.totalBytes = values; 326 | if (!this.showHeader) { 327 | this.totalBytes = this.header.concat(values); 328 | } 329 | this.drawRawBytes(); 330 | } 331 | 332 | if (this.editMode == 'chroma') { 333 | // Extract components, each (scale) numbers is Y, then Cb and Cr. 334 | let scale = this.scale; 335 | let values = this.getValuesFromEditor(); 336 | let Y = []; 337 | let Cb = []; 338 | let Cr = []; 339 | for (let i = 0; i < values.length; i+= scale + 2) { 340 | for (let j = 0; j < scale; j++) { 341 | Y.push(values[i + j]) 342 | } 343 | Cb.push(values[i + scale]) 344 | Cr.push(values[i + scale + 1]); 345 | } 346 | 347 | // To draw these components, we copy them back into the decoded image 348 | // and redraw it. 349 | this.fillDecodedComponent('Y', Y); 350 | this.fillDecodedComponent('Cb', Cb); 351 | this.fillDecodedComponent('Cr', Cr); 352 | 353 | let decodedImage = this.decodedImage; 354 | 355 | decodedImage._decoder.copyToImageData({ 356 | width: decodedImage.width, 357 | height: decodedImage.height, 358 | data: decodedImage.data 359 | }); 360 | this.drawDecodedImage(); 361 | } 362 | 363 | if (this.editMode == 'full-dctLuminance') { 364 | let values = this.getValuesFromEditor(); 365 | 366 | if (this.numberOfCoefficients != undefined) { 367 | // Fill in missing numbers with 0. 368 | // This allows the user to easily 0 out chunks at the bottom by deleting them. 369 | for (let i = values.length; i < this.numberOfCoefficients; i++) { 370 | values[i] = 0; 371 | } 372 | } 373 | 374 | values = this.inverseDct(values); 375 | this.drawLuminance(values, this.decodedImage.width, this.decodedImage.height); 376 | } 377 | 378 | if (this.editMode == 'dctLuminance') { 379 | // Put the edited coefficients back into the image. 380 | // In the editor, each block MUST be on its own line 381 | let text = this.editor.getSession().getValue(); 382 | let editorBlocks = text.trim().split('\n'); 383 | let rawYBlocks = this.decodedImage._decoder.frames[0].components[componentMap['Y'] + 1].blocks; 384 | let Yblocks = []; 385 | for (let i = 0; i < rawYBlocks.length; i++) { 386 | for (let j = 0; j < rawYBlocks[i].length; j++) { 387 | Yblocks.push(rawYBlocks[i][j]); 388 | } 389 | } 390 | 391 | for (let i = 0; i < Yblocks.length; i++) { 392 | let block = ''; 393 | if (i <= editorBlocks.length - 1) block = editorBlocks[i]; 394 | let Yblock = Yblocks[i]; 395 | let values = block.trim().split(' '); 396 | for (let j = 0; j < 64; j++) { 397 | let val = 0; 398 | if (j <= values.length - 1) val = Number(values[j]); 399 | Yblock[j] = val; 400 | } 401 | } 402 | 403 | this.reEncodeImage(); 404 | this.drawDecodedImage(); 405 | } 406 | 407 | if (this.editMode == 'justY' || this.editMode == 'justCb' || this.editMode == 'justCr') { 408 | let components = ['Cb', 'Cr']; 409 | if (this.editMode == 'justCb') components = ['Y', 'Cr']; 410 | if (this.editMode == 'justCr') components = ['Y', 'Cb']; 411 | 412 | for (let comp of components) { 413 | let rawBlocks = this.decodedImage._decoder.frames[0].components[componentMap[comp] + 1].blocks; 414 | for (let i = 0; i < rawBlocks.length; i++) { 415 | for (let j = 0; j < rawBlocks[i].length; j++) { 416 | rawBlocks[i][j] = 0; 417 | } 418 | } 419 | } 420 | 421 | this.reEncodeImage(); 422 | this.drawDecodedImage(); 423 | } 424 | 425 | if (this.editMode == 'dctBlue' || this.editMode == 'dctRed') { 426 | let text = this.editor.getSession().getValue(); 427 | let editorBlocks = text.trim().split('\n'); 428 | let component = this.editMode == 'dctBlue' ? 'Cb' : 'Cr'; 429 | let rawBlocks = this.decodedImage._decoder.frames[0].components[componentMap[component] + 1].blocks; 430 | let blocks = []; 431 | for (let i = 0; i < rawBlocks.length; i++) { 432 | for (let j = 0; j < rawBlocks[i].length; j++) { 433 | blocks.push(rawBlocks[i][j]); 434 | } 435 | } 436 | 437 | if (blocks.length != editorBlocks.length) { 438 | // TODO: Maybe make up the rest by filling with 0's? 439 | console.error("Expected", blocks.length, "blocks. Found", editorBlocks.length + "."); 440 | } 441 | 442 | for (let i = 0; i < editorBlocks.length; i++) { 443 | let block = editorBlocks[i]; 444 | let originalBlock = blocks[i]; 445 | let values = block.trim().split(' '); 446 | for (let j = 0; j < 64; j++) { 447 | originalBlock[j] = Number(values[j]) 448 | } 449 | } 450 | 451 | this.reEncodeImage(); 452 | this.drawDecodedImage(); 453 | } 454 | 455 | // Update byte counter 456 | this.updateByteCounter(); 457 | } 458 | 459 | updateByteCounter() { 460 | if (this.byteCounter == undefined) return; 461 | let values = this.getValuesFromEditor(); 462 | this.byteCounter.innerHTML = "Size: " + (values.length / 1000).toFixed(2) + " kb."; 463 | this.byteCounter.innerHTML += " Dimensions: " + this.imageWidth + " x " + this.imageHeight; 464 | } 465 | 466 | reEncodeImage() { 467 | let decodedImage = this.decodedImage; 468 | 469 | decodedImage._decoder.components = []; 470 | let frame = decodedImage._decoder.frames[0]; 471 | for (var i = 0; i < frame.componentsOrder.length; i++) { 472 | var component = frame.components[frame.componentsOrder[i]]; 473 | decodedImage._decoder.components.push({ 474 | lines: decodedImage._decoder.buildComponentData(frame, component), 475 | scaleX: component.h / frame.maxH, 476 | scaleY: component.v / frame.maxV 477 | }); 478 | } 479 | decodedImage._decoder.copyToImageData({ 480 | width: decodedImage.width, 481 | height: decodedImage.height, 482 | data: decodedImage.data 483 | }); 484 | 485 | this.imageWidth = decodedImage.width; 486 | this.imageHeight = decodedImage.height; 487 | this.updateByteCounter(); 488 | } 489 | 490 | getLineLength() { 491 | return this.editor.getSession().getValue().trim().split('\n')[0].length; 492 | } 493 | 494 | getValuesFromEditor(overrideText) { 495 | // Takes the text in the editor and converts it to an array of numbers. 496 | let text = overrideText; 497 | if (text == undefined) { 498 | text = this.editor.getSession().getValue(); 499 | } 500 | let lines = text.trim().split('\n'); 501 | let values = []; 502 | for (let line of lines) { 503 | let characters = line.trim().split(' '); 504 | for (let char of characters) { 505 | values.push(Number(char)); 506 | } 507 | } 508 | 509 | return values; 510 | } 511 | 512 | setResetText(text) { 513 | this.resetText = text; 514 | this.resetButton.onclick = function(event) { 515 | let instance = event.target.editorInstance; 516 | instance.currentValues = instance.finalValues;// Resets the DCT animations 517 | instance.editor.setValue(instance.resetText, -1); 518 | 519 | if (window.currentInterval) { 520 | clearInterval(window.currentInterval); 521 | window.currentInterval = undefined; 522 | } 523 | } 524 | } 525 | 526 | putValuesInEditor(values, linebreakIndex, useForReset) { 527 | // Puts the values separated by space, and optionally line breaks, in the editor 528 | let text = ''; 529 | if (linebreakIndex == undefined) linebreakIndex = -1; 530 | 531 | for (let i = 0; i < values.length; i ++) { 532 | if (linebreakIndex != -1 && i != 0 && i % linebreakIndex == 0) { 533 | text += '\n'; 534 | } 535 | text += values[i] + ' '; 536 | } 537 | 538 | if (useForReset) { 539 | this.setResetText(text); 540 | } 541 | 542 | this.editor.setValue(text, -1); 543 | } 544 | 545 | createImageEditor(containerElement, title) { 546 | 547 | let outerContainer = containerElement; 548 | 549 | /** 550 | * Control bar section 551 | */ 552 | let headerContainerElement = document.createElement('div'); 553 | headerContainerElement.className = 'image-editor-header'; 554 | outerContainer.appendChild(headerContainerElement); 555 | 556 | // Size counter 557 | this.byteCounter = document.createElement('div'); 558 | this.byteCounter.innerHTML = "Size: 0 kb"; 559 | 560 | // Reset button 561 | let resetContainer = document.createElement('div'); 562 | this.resetButton = document.createElement('button'); 563 | resetContainer.appendChild(this.resetButton); 564 | this.resetButton.innerHTML = 'Reset'; 565 | this.resetButton.editorInstance = this; 566 | 567 | // Title 568 | this.editorTitle = document.createElement('div'); 569 | if (title == undefined) title = 'JPEG Editor'; 570 | this.editorTitle.innerHTML = title; 571 | 572 | headerContainerElement.appendChild(resetContainer); 573 | headerContainerElement.appendChild(this.editorTitle); 574 | headerContainerElement.appendChild(this.byteCounter); 575 | 576 | /** 577 | * Content section 578 | */ 579 | containerElement = document.createElement('div'); 580 | containerElement.className = 'image-editor'; 581 | outerContainer.appendChild(containerElement); 582 | 583 | 584 | // Create ace editor 585 | let editorContainer = document.createElement('div'); 586 | editorContainer.className = 'byte-editor'; 587 | 588 | 589 | let editorElement = document.createElement('div'); 590 | editorContainer.appendChild(editorElement); 591 | editorElement.className = 'ace-editor'; 592 | containerElement.appendChild(editorContainer); 593 | 594 | 595 | let editor = ace.edit(editorElement, { 596 | useWorker: false 597 | }); 598 | 599 | this.editor = editor; 600 | let that = this; 601 | editor.setTheme("ace/theme/monokai"); 602 | editor.session.setMode("ace/mode/assembly_x86"); 603 | editor.getSession().setUseWrapMode(true); 604 | editor.getSession().on('change', function() { 605 | if (editor.getValue().length == 0) 606 | return; 607 | that.onEditorChange(); 608 | }); 609 | // Create canvas 610 | let viewerElement = document.createElement('div'); 611 | let canvas = document.createElement('canvas'); 612 | this.canvas = canvas; 613 | viewerElement.appendChild(canvas); 614 | containerElement.appendChild(viewerElement); 615 | viewerElement.className = 'image-viewer'; 616 | 617 | if (this.highlightPixelOnClick) { 618 | canvas.editor = this.editor; 619 | canvas.addEventListener("click", function(event) { 620 | var rect = event.target.getBoundingClientRect(); 621 | var x = event.clientX - rect.left; //x position within the element. 622 | var y = event.clientY - rect.top; //y position within the element. 623 | 624 | // Need to know which component, to get the scale. 625 | let componentData = that.decodedImage._decoder.frames[0].components[componentMap['Y'] + 1];// Hardcoded to 'Y'; 626 | let scale = 1; // You can compute the scale for a component by doing 627 | // scaleX: component.h / frame.maxH 628 | // scaleY: component.v / frame.maxV 629 | 630 | // Then divide x and y by 8 631 | x /= 8; y/= 8; 632 | x /= scale; y/= scale; 633 | let cssScaleX = canvas.width / canvas.offsetWidth; 634 | let cssScaleY = canvas.height / canvas.offsetHeight; 635 | x *= cssScaleX; 636 | y *= cssScaleY; 637 | 638 | // Then the line no is x * blocksPerLine + y * blocksPerColumn 639 | let lineNo = Math.floor(x) + Math.floor(y) * componentData.blocksPerLine; 640 | event.target.editor.scrollToLine(lineNo); 641 | }); 642 | } 643 | 644 | // let clearDiv = document.createElement('div'); 645 | // clearDiv.style = 'clear:both;'; 646 | // containerElement.appendChild(clearDiv); 647 | } 648 | 649 | drawRawBytes() { 650 | const byteArray = new Uint8Array(this.totalBytes); 651 | // Take the header and body bytes and draw them. 652 | //const byteArray = new Uint8Array((this.header + ',' + this.body).split(',').map(parseFloat)); 653 | let blob = new Blob([byteArray.buffer], {type: 'image/jpeg'}); 654 | let that = this; 655 | 656 | createImageBitmap(blob) 657 | .then(function(imageBitmap) { 658 | let canvas = that.canvas; 659 | canvas.width = imageBitmap.width; 660 | canvas.height = imageBitmap.height; 661 | 662 | that.imageWidth = imageBitmap.width; 663 | that.imageHeight = imageBitmap.height; 664 | that.updateByteCounter(); 665 | 666 | let ctx = canvas.getContext('2d'); 667 | ctx.drawImage(imageBitmap, 0, 0, imageBitmap.width, imageBitmap.height); 668 | }) 669 | .catch(function(error) { 670 | console.log(error); 671 | let canvas = that.canvas; 672 | let ctx = canvas.getContext('2d'); 673 | let corruptedImage = that.corruptedImage; 674 | ctx.drawImage(corruptedImage, 0, 0, canvas.width, canvas.height); 675 | }) 676 | } 677 | 678 | drawPixelData(pixelData, width, height) { 679 | let canvas = this.canvas; 680 | canvas.width = width; 681 | canvas.height = height; 682 | let ctx = canvas.getContext('2d'); 683 | let imageData = new ImageData(new Uint8ClampedArray(pixelData), width, height); 684 | ctx.putImageData(imageData, 0, 0); 685 | } 686 | 687 | drawDecodedImage(mode) { 688 | let canvas = this.canvas; 689 | let image = this.decodedImage; 690 | 691 | canvas.width = image.width; 692 | canvas.height = image.height; 693 | this.imageWidth = image.width; 694 | this.imageHeight = image.height; 695 | this.updateByteCounter(); 696 | 697 | var ctx = canvas.getContext('2d'); 698 | var imageData = new ImageData(new Uint8ClampedArray(image.data), image.width, image.height); 699 | 700 | if (mode != undefined) { 701 | let indices = [1, 2]; 702 | if (mode == 'justG') { 703 | indices = [0, 2]; 704 | } else if (mode == 'justB') { 705 | indices = [0, 1]; 706 | } 707 | 708 | for (let i = 0; i < image.data.length; i+= 4) { 709 | for (let index of indices) image.data[i+index] = 0; 710 | } 711 | imageData = new ImageData(new Uint8ClampedArray(image.data), image.width, image.height); 712 | } 713 | 714 | ctx.putImageData(imageData, 0, 0); 715 | } 716 | 717 | drawYCbCr(Y, Cb, Cr) { 718 | let width = this.imageWidth; 719 | let height = this.imageHeight; 720 | let pixels = new Uint8ClampedArray(width * height * 4); 721 | 722 | for(let i = 0; i < Y.length; i++) { 723 | let index = i * 4; 724 | let rgb = this.ycbcrToRgb(Y[i], Cb[i], Cr[i]); 725 | pixels[index] = rgb.R; 726 | pixels[index + 1] = rgb.G; 727 | pixels[index + 2] = rgb.B; 728 | pixels[index + 3] = 255; 729 | } 730 | 731 | let canvas = this.canvas; 732 | canvas.width = width; 733 | canvas.height = height; 734 | var ctx = canvas.getContext('2d'); 735 | var imageData = new ImageData(new Uint8ClampedArray(pixels), width, height); 736 | ctx.putImageData(imageData, 0, 0); 737 | } 738 | 739 | drawLuminance(yValues, width, height) { 740 | let canvas = this.canvas; 741 | 742 | canvas.width = width; 743 | canvas.height = height; 744 | this.imageWidth = canvas.width; 745 | this.imageHeight = canvas.height; 746 | this.updateByteCounter(); 747 | var ctx = canvas.getContext('2d'); 748 | var pixels = new Uint8ClampedArray(width * height * 4); 749 | 750 | for (let i = 0; i < yValues.length; i++) { 751 | let index = i * 4; 752 | let Y = yValues[i]; 753 | pixels[index] = Y; 754 | pixels[index + 1] = Y; 755 | pixels[index + 2] = Y; 756 | pixels[index + 3] = 255; 757 | } 758 | 759 | var imageData = new ImageData(pixels, width, height); 760 | ctx.putImageData(imageData, 0, 0); 761 | } 762 | 763 | rgbToYcbcr(r, g, b) { 764 | 765 | } 766 | 767 | ycbcrToRgb(Y, Cb, Cr) { 768 | let R = clampTo8bit(Y + 1.402 * (Cr - 128)); 769 | let G = clampTo8bit(Y - 0.3441363 * (Cb - 128) - 0.71413636 * (Cr - 128)); 770 | let B = clampTo8bit(Y + 1.772 * (Cb - 128)); 771 | return {R:R, G:G, B:B}; 772 | } 773 | 774 | fill(array, value, start, end) { 775 | if (value == undefined) value = 0; 776 | if (start == undefined) start = 0; 777 | if (end == undefined) end = array.length; 778 | 779 | for (let i = start; i < end; i++) { 780 | array[i] = value; 781 | } 782 | 783 | return array; 784 | } 785 | 786 | getDecodedComponent(component) { 787 | let lines = this.decodedImage._decoder.components[componentMap[component]].lines; 788 | let values = []; 789 | for (let i = 0; i < lines.length; i++) { 790 | //if ( i >= this.decodedImage.height) break; // Account for byte stuffing? 791 | for (let j = 0; j < lines[i].length; j++) { 792 | //if (j >= this.decodedImage.width) break; 793 | values.push(lines[i][j]) 794 | } 795 | } 796 | return values; 797 | } 798 | 799 | getDctComponent(component) { 800 | let blocks = this.decodedImage._decoder.frames[0].components[componentMap[component] + 1].blocks; 801 | let values = []; 802 | for (let i = 0; i < blocks.length; i++) { 803 | for (let j = 0; j < blocks[i].length; j++) { 804 | for (let k = 0; k < blocks[i][j].length; k++) { 805 | values.push(blocks[i][j][k]); 806 | } 807 | } 808 | } 809 | 810 | return values; 811 | } 812 | 813 | forwardDct(values, round) { 814 | // Takes an array of length n^2 and returns n^2 unscaled DCT coefficients that represent these values. 815 | // If the given array is not of length n^2, it will be padded with 0's. 816 | let valuesCopy = values.slice(0, values.length); 817 | pad(valuesCopy); 818 | 819 | fastDctLee.transform(valuesCopy); 820 | if (round) { 821 | for (let i = 0; i < valuesCopy.length; i++) { 822 | valuesCopy[i] = Math.round(valuesCopy[i]); 823 | } 824 | } 825 | return valuesCopy; 826 | } 827 | 828 | inverseDct(coefficients) { 829 | // Takes an array of n^2 coefficients, and reconstructs the original n^2 values. 830 | // If the given array is not of length n^2, it will be padded with 0's. 831 | var coefficientsCopy = coefficients.slice(0, coefficients.length); 832 | pad(coefficientsCopy); 833 | 834 | fastDctLee.inverseTransform(coefficientsCopy); 835 | // Scale back the numbers 836 | let scale = coefficientsCopy.length / 2; 837 | for (let i = 0; i < coefficientsCopy.length; i++) { 838 | coefficientsCopy[i] /= scale; 839 | // Round the numbers for aesthetic reasons. This is a lossy step. 840 | coefficientsCopy[i] = Math.round(coefficientsCopy[i]); 841 | } 842 | 843 | return coefficientsCopy; 844 | } 845 | } 846 | 847 | module.exports = ImageUtilities; -------------------------------------------------------------------------------- /index.idyll: -------------------------------------------------------------------------------- 1 | [meta title:"Unraveling The JPEG" description:"JPEG images are everywhere in our digital lives, but behind the veil of familiarity lie algorithms that remove details that are imperceptible to the human eye. This produces the highest visual quality with the smallest file size—but what does that look like? Let's see what our eyes can't see!" 2 | shareImageUrl:"https://parametric.press/issue-01/unraveling-the-jpeg/static/images/share.png" 3 | shareImageWidth:"880" 4 | shareImageHeight:"440" /] 5 | 6 | [var name:"parametricSlug" value:"unraveling-the-jpeg" /] 7 | 8 | [Nav fullWidth:true /] 9 | 10 | [Header 11 | title:`["Unraveling", "the JPEG"]` 12 | longTitle:`["Unraveling", "the JPEG"]` 13 | date:"May 1, 2019" 14 | dek:"JPEG images are everywhere in our digital lives, but behind the veil of familiarity lie algorithms that remove details that are imperceptible to the human eye. This produces the highest visual quality with the smallest file size—but what does that look like? Let's see what our eyes can't see!" 15 | fullWidth:true 16 | headerImage:"static/images/luxury.png" 17 | authors:`[{ 18 | name: "Omar Shehata", 19 | role: 'Author & Developer', 20 | url: "https://omarshehata.me/" 21 | }]` 22 | doi:"https://doi.org/10.5281/zenodo.2655041" 23 | archive:`'https://parametric-press-archives.s3.amazonaws.com/issue-01/' + parametricSlug + '.warc.gz'` 24 | source:`"https://github.com/ParametricPress/01-" + parametricSlug ` 25 | /] 26 | 27 | It's easy to take for granted that you can send a picture to a friend without worrying about what device, browser, or operating system they’re using, but things weren’t always this way. By the early 1980s, computers could store and display digital images, but there were many competing ideas about how best to do that. You couldn't just send an image from one computer to another and expect it to work. 28 | 29 | To solve this problem, the Joint Photographic Experts Group (JPEG), a committee of experts from all over the world, was established in 1986 as a joint effort by the ISO (International Organization for Standardization) and the IEC (International Electrotechnical Commission)—two international standards organizations headquartered in Geneva, Switzerland. 30 | 31 | JPEG, the group of people, created JPEG, a standard for digital image compression, in 1992. Anyone who's ever used the internet has probably seen a JPEG-encoded image. It is by far the most ubiquitous way of encoding, sending and storing images. From web pages to email to social media, JPEG is used billions of times a day—almost every time we view or send images online. Without JPEG, the web would be a little less colorful, a lot slower, and probably have far fewer cat pictures! 32 | 33 | This article is about how to decode a JPEG image. In other words, it's about what it takes to convert the compressed data stored on your computer to the image that appears on the screen. It's worth learning about not just because it's important to understand the technology we all use everyday, but also because, as we unravel the layers of compression, we learn a bit about perception and vision, and about what details our eyes are most sensitive to. 34 | 35 | It's also just a lot of fun to play with images this way. 36 | 37 | [br/] 38 | 39 | [FullWidth] 40 | [img style:`{width: '100%'}` src:"static/images/glitchy-cat.gif"/] 41 | [/FullWidth] 42 | 43 | [br/] 44 | 45 | ## Peering Inside a JPEG 46 | 47 | Everything on a computer is stored as a series of binary numbers. Typically, these bits, the zeros and ones, are arranged in groups of eight, known as bytes. When you open a JPEG image on your computer, something (the browser, your operating system, or something else) has to decode the bytes to recover the original image as a list of colors that can then be displayed. 48 | 49 | [div] 50 | If you download [Reference content:"The adorable kitten image used in this article was taken by Mikhail Vasilyev."] that picture of the cat [/Reference] and open it using any text editor, you'll see a bunch of garbled characters. 51 | [/div] 52 | 53 | [br/] 54 | 55 | 56 | [img src:"static/images/blanket-cat-notepad.png"/] 57 | [Caption] 58 | Here I'm using [a href:"https://notepad-plus-plus.org/"]Notepad++[/a] to look at the image file, since common text editors like Windows' Notepad will change the file's binary contents when you save it so it's no longer a valid JPEG. 59 | [/Caption] 60 | 61 | [br/] 62 | 63 | By opening an image in a text editor, you’ve confused the computer, in the same way you confuse your brain when you rub your eyes too hard and start to see blotches of dimness and color! 64 | 65 | These blotches you see—known as [a href:"https://en.wikipedia.org/wiki/Phosphene" target:"_blank"]phosphenes[/a]—don't come from any light stimulus, nor are they hallucinations made up in your mind. They arise because your brain assumes that any electrical signal arriving through the nerves in your eye is conveying light information. The brain needs to make this assumption because there’s no way to know whether a given signal is sound, sight, or something else. All the nerves in your body carry exactly the same type of electrical pulse. When you apply pressure by rubbing your eyes, you’re sending non-visual signals that trigger the receptors in your eye, which your brain interprets—incorrectly, in this case—as vision. You can literally see the pressure! 66 | 67 | It’s fun to think about how similar computers are to our brains, but this is also a useful analogy because it illustrates how much the meaning of data—whether carried through a body by nerves or stored in a computer—relies on how you interpret it. All binary data is made up of ones and zeros, basic components that could be conveying any kind of information. Your computer often guesses how to interpret it using clues, like the file extension. Here we’ve forced it to interpret it as text, because that’s what a text editor expects. 68 | 69 | To understand how a JPEG image is decoded we need to see the original signals themselves—the binary data. This can be done with a [Tooltip content:"You don't necessarily need a hex editor. You can edit the characters as text, save the file, and re-open it as an image just fine. But since our goal is to understand how JPEG stores images, it's helpful to be able to actually read the bytes!"]hex editor[/Tooltip], or it can be done right here in this webpage! Below is the image next to [Tooltip content:"The only thing omitted here is the header, which is a section of bytes at the beginning of any JPEG image that contain metadata like the width and height. We'll revist this later in the article."]all of its bytes[/Tooltip], represented as decimal numbers. You can make changes to the bytes, and it will re-decode and display the new, edited image as you type. 70 | 71 | [RawEditor ref:"Raw1" fullWidth:true imageUrl:"static/images/blanket-cat.jpg" showHeader:false maxWidth:700 /] 72 | 73 | [aside] 74 | _Hint: try scrolling down and removing a few chunks. Don't worry, you can always reset the image back to the original!_[br/] 75 | [/aside] 76 | 77 | There's a lot you can learn just from playing around with this editor. For example, can you figure out the order the pixels are stored in? 78 | 79 | Something strange in the example above is that changing some numbers doesn't seem to impact the image at all, while [EditorLink editor:`refs.Raw1` line:1 search:' 17 ' replace:' 0 ']setting the 17 on line one to 0[/EditorLink] completely ruins the image! Other actions, like [EditorLink editor:`refs.Raw1` line:1988 search:' 7 ' replace:' 254 ']setting the 7 on line 1988 to 254[/EditorLink] change the color, but only for subsequent pixels. 80 | 81 | [Aside] 82 | [Recirc slug:parametricSlug /] 83 | [/Aside] 84 | 85 | Perhaps what's most peculiar is that some numbers change not just the color but the shape of the image as well. Change the [EditorLink editor:`refs.Raw1` line:12 search:' 70 ' replace:' 2 ']70 in line 12 to 2[/EditorLink] and look at the top row of the image to see what I mean. No matter what JPEG image you're using, you'll always find these mysterious checkerboard patterns when editing the bytes. 86 | 87 | It's hard to decipher how the image can be reconstructed from these bytes by playing around like this, because JPEG compression is actually composed of three different compression techniques, which are applied in successive layers. We'll look at each of these layers of compression separately to unravel this mysterious behavior we're seeing. 88 | 89 | [inset] 90 | 91 | ### The three layers of JPEG compression 92 | 1. Chrominance Subsampling 93 | 2. Discrete Cosine Transform & Quantization 94 | 3. Run-Length, Delta & Huffman Encoding 95 | 96 | [/inset] 97 | 98 | 99 | To give you an idea of the scale of this compression, notice that the image above is represented using exactly 79,819 numbers, which makes it about 79 kilobytes. If it were stored with no compression, three numbers would be needed for each pixel—one for each of the red, green and blue components. That would mean a total of 917,700 numbers, or about 917 kilobytes. With JPEG compression, the resulting file is over **ten times smaller**! 100 | 101 | In fact, this image can be squeezed into far fewer bytes. Below is the image next to a version of it that was compressed down to just 16 kilobytes, which makes it **fifty-seven times smaller** than the uncompressed version would be! 102 | 103 | [img src:"static/images/compression-compare.png"/] 104 | [br/] 105 | 106 | If you look closely you'll see that these images are not identical. Both are JPEG-encoded images, but the one on the right is much smaller in terms of file size. It also doesn't look as nice (notice how blocky the colors look in the background). This is why JPEG is known as a **lossy** compression technique; the image changes and loses some detail as a result of the compression. 107 | 108 | ## 1. Chrominance Subsampling 109 | 110 | Here's the image with just the first layer of compression applied. 111 | 112 | [ChromaEditor fullWidth:true ref:"Chroma1" imageUrl:"static/images/blanket-cat-smallest.jpg" maxWidth:400 /] 113 | 114 | [aside] 115 | _Hint: Notice that removing one number ruins all the colors. But removing exactly 6, or any multiple of 6, has a minimal effect on the image._[br/] 116 | [/aside] 117 | 118 | It's a little more straightforward to decipher now. It's almost a simple list of colors, where each byte changes exactly one pixel, and yet it's already almost twice as small as the uncompressed image (which would be around 300 kb for this smaller size). Can you guess why? 119 | 120 | You can tell that these numbers don't represent the standard red, green and blue components because [EditorLink editor:`refs.Chroma1` pattern:'zero-all']replacing all the numbers with 0[/EditorLink] turns the image green (as opposed to black). 121 | 122 | This is because these bytes represent the [EditorLink editor:`refs.Chroma1` pattern:'isolate-Y' scale:4]Y[/EditorLink] (brightness), [EditorLink editor:`refs.Chroma1` pattern:'isolate-Cb' scale:4]Cb[/EditorLink] (relative blueness), and [EditorLink editor:`refs.Chroma1` pattern:'isolate-Cr' scale:4]Cr[/EditorLink] (relative redness) of the image. 123 | 124 | Why not just use RGB? After all, that's how most modern screens work. Your monitor can display any color by turning on red, green and blue lights at various intensities for each pixel. White is displayed by turning on all three colors at full brightness, while black is displayed by turning them all off. 125 | 126 | [br/] 127 | [img src:"static/images/pixels.png"/] 128 | // [caption]Adapted from http://www.chem.purdue.edu/jmol/cchem/RGBColors/body_rgbcolors.html. [/caption] 129 | [br/] 130 | 131 | That's also very similar to how human eyes work. The color receptors in our eyes known as “cones” are split into three types, each of which is mostly sensitive either to red, green, or blue. Rods, the other type of receptor we have in our eyes, can only detect changes in brightness, but they’re far more sensitive. We have about one hundred and twenty million rods in our eyes, compared to a measly six million cones. 132 | 133 | This means that our eyes are much better at detecting changes in brightness than they are at detecting changes in color. If we can separate the color from the brightness, we can remove a bit of the color without anyone noticing. **Chrominance subsampling** is the process of representing an image's color components at a lower resolution than its luminance components. In the example above, each pixel has exactly one Y component, while each discrete group of four pixels has exactly one Cb and one Cr component. So the image contains only a quarter as much color information as it originally did. 134 | 135 | [aside] 136 | Using the YCbCr colorspace is not unique to JPEG. In fact, it was originally [a href:"https://en.wikipedia.org/wiki/GeorgesValensi"]developed in 1938 for TV broadcasts[/a]. Not everyone had color TVs, so separating out the color from the luminance allowed everyone to receive the same transmission, and TVs that didn't support color would just use the luminance component. 137 | [/aside] 138 | 139 | This is why removing one number from the editor above completely ruins the color. Here, the [Tooltip content:"In general, JPEG images need not store the components in any particular order. They might be interleaved like in this example, or all the Y components of the image could be stored followed by all the Cb etc. The header of the JPEG is what tells us how the components are stored."]components are stored[/Tooltip] as **Y Y Y Y Cb Cr**. Removing the first number causes the Cb value to be interpreted as Y, the Cr as Cb, and creates a ripple effect that flips all the colors across the image. 140 | 141 | There's nothing in the JPEG specification that says you must use YCbCr. Most JPEG images you'll find use it because it tends to produce higher quality images after subsampling compared to RGB. You don't have to take this for granted though. You can see for yourself in the grid below what it looks like to subsample each component individually across RGB as well as YCbCr. 142 | 143 | [var name:"subsamplePercent" value:0.15 /] 144 | 145 | [SubsampleGrid imageUrl:"static/images/blanket-cat-tiny.jpg" subsamplePercent:`subsamplePercent ` maxWidth:200 /] 146 | [div className:"subsample-controls" ] 147 | Subsample percent: [Display value:subsamplePercent format:".0%" /] 148 | [Range value:subsamplePercent min:0 max:1 step:0.05 /] 149 | [br/] 150 | Move slider to adjust the amount of subsampling applied. 151 | [/div] 152 | 153 | [aside] 154 | Removing a bit of blue isn't as noticeable as removing red or green. This is because of the six million cones you have in your eyes, about 64% are most sensitive to red, 32% to green, and only 2% to blue. 155 | [/aside] 156 | 157 | Subsampling the Y component (bottom left) has the greatest effect on the image quality. Even a tiny bit is already noticeable. You can move the slider to see how removing a greater percentage of each component affects the image. 158 | 159 | Converting an image from RGB to YCbCr doesn't make the file size any smaller, but it does make it easier to find less noticeable details to remove. It's that second step where the actual lossy compression happens. This idea of finding new ways to represent data to make it more compressible is at the heart of what the next layer does. 160 | 161 | ## 2. Discrete Cosine Transform & Quantization 162 | 163 | This layer of compression is largely the defining feature of JPEG. After the colors are converted to YCbCr, the components are compressed individually, so we can focus on just the Y component for the rest of the article. Here's what the bytes for the Y component look like with this layer applied. 164 | 165 | [DctEditor fullWidth:true ref:"DCT1" comp:"Y" imageUrl:"static/images/blanket-cat-smallest.jpg" maxWidth:400 /] 166 | 167 | [aside] 168 | Hint: Try clicking on any pixel in the image to see the line in the editor that represents it. Try removing numbers from the end, or adding a few zeros to any individual number to make its effect more obvious. 169 | [/aside] 170 | 171 | At first glance, this seems like very poor compression. There are 100,000 pixels in this image, and yet it takes 102,400 numbers to represent the luminance of each pixel—that's worse than not compressing it at all! 172 | 173 | But notice that most of these values are 0. In fact, [EditorLink editor:`refs.DCT1` pattern:"remove-zeros"]all of these trailing zeros can be removed[/EditorLink] with no change to the image. This leaves only about 26,000 numbers, which makes it about four times smaller! 174 | 175 | In this layer lies the secret to the checkerboard patterns. Unlike the other effects we've seen, the appearance of these patterns is not a glitch. They are in fact the building blocks of the entire image. Every line in the editor above contains exactly 64 numbers, known as the Discrete Cosine Transform (DCT) coefficients, which correspond to intensities of 64 unique patterns. 176 | 177 | These patterns are formed out of cosine waves. Here's what a few of them look like: 178 | 179 | [img src:"static/images/dct_coefficients.png"/] 180 | [caption] 181 | These are 8 of the 64 discrete cosine transform coefficients. Credit: [a href:"http://www.jezzamon.com/fourier/index.html"]Jez Swanson[/a]. 182 | [/caption] 183 | 184 | Below is an image that shows all 64 of them individually. 185 | 186 | [data name:"DCT-grid" source:"DCT-grid.json" /] 187 | [data name:"DCT-circle" source:"DCT-circle.json" /] 188 | [data name:"DCT-eye" source:"DCT-eye.json" /] 189 | [DctEditor fullWidth:true ref:"DCT2" imageUrl:"static/images/DCTGrid.jpg" override:DCT-grid isUrlExempt:true/] 190 | 191 | These patterns are special because they form a basis for 8x8 images. If you're not familiar with linear algebra, what that means is that any 8x8 image, anything at all that you can imagine, can be made out of these specific 64 patterns. The Discrete Cosine Transform is the process of breaking up the image into 8x8 blocks and converting each block into a combination of these 64 coefficients. Here's [EditorLink editor:`refs.DCT2` pattern:"animate-DCT" DCT:DCT-circle]how you would form a circle[/EditorLink] by combining these patterns, or [EditorLink editor:`refs.DCT2` pattern:"animate-DCT" DCT:DCT-eye]the cat's face[/EditorLink]. You can [EditorLink editor:`refs.DCT2` pattern:"animate-DCT" DCT:DCT-grid]click here to go back to the grid of 64 patterns[/EditorLink]. 192 | 193 | It seems like magic to say that any image can be represented using 64 specific patterns. But this is the same thing as saying any location on the Earth can be represented using only two numbers: longitude and latitude. We often treat the surface of the Earth as two-dimensional, so only two numbers are needed. An 8x8 image is sixty-four-dimensional, so we need sixty-four numbers. 194 | 195 | In terms of compression, it’s not obvious how this helps us. If we need sixty-four numbers to represent an 8x8 image, why is this better than storing the sixty-four luminance components? We do it for the same reason we converted from the three numbers of RGB to the three numbers of YCbCr: it allows us to remove detail that’s less noticeable. 196 | 197 | It's hard to see exactly what the details that are removed in this compression step look like because JPEG only applies the Discrete Cosine Transform to blocks of 8x8 pixels at a time. However, there’s no reason we can’t apply it to the whole image. Here’s what it looks like to apply the DCT to the Y component of the entire image: 198 | 199 | [FullDctEditor ref:"DCT3" fullWidth:true imageUrl:"static/images/blanket-cat-smallest.jpg" maxWidth:400 /] 200 | 201 | We can [EditorLink editor:`refs.DCT3` pattern:"remove-{170}"]remove over 60,000 numbers[/EditorLink] from the end with almost no noticeable change. But notice that if [EditorLink editor:`refs.DCT3` pattern:"zero-{5}"]we set just the first five numbers to zero[/EditorLink] (ignoring the first because it just makes the image darker) there's already an obvious difference. 202 | 203 | [Aside] 204 | [Newsletter /] 205 | [/Aside] 206 | 207 | The numbers at the beginning represent the lower frequency changes in the image, which our eyes are better at detecting. The numbers towards the end represent the higher frequency changes, which are harder for us to see, so we don't notice when they're gone. To see "what our eyes can't see", we can isolate these high frequency details by setting the [EditorLink editor:`refs.DCT3` pattern:"zero-{5000}"]first 5,000 numbers to zero[/EditorLink]. 208 | 209 | What you're looking at here is all the areas of the image that have the greatest change from one pixel to the next. The cat's eyes, whiskers, fuzzy blanket and shadows in the bottom left corner all stand out. This can be taken even further, to setting the first [EditorLink editor:`refs.DCT3` pattern:"zero-{10000}"]10,000 numbers to zero[/EditorLink]; [EditorLink editor:`refs.DCT3` pattern:"zero-{20000}"]20,000[/EditorLink]; [EditorLink editor:`refs.DCT3` pattern:"zero-{40000}"]40,000[/EditorLink] or [EditorLink editor:`refs.DCT3` pattern:"zero-{60000}"]60,000[/EditorLink]. 210 | 211 | These high frequency details are what JPEG removes during this compression step. Converting the colors to the DCT coefficients is not a lossy operation. It's the quantization step that's lossy, where values that are high frequency, close to zero, or both, are removed. When you select a lower quality setting when creating a JPEG image, it increases the threshold for how many of these values are removed, which leads to a smaller file size but a blockier image. This is why the version of the image in the first section that was 57 times smaller looked blocky. Each 8x8 block was represented by far fewer DCT coefficients compared to the higher quality version. 212 | 213 | [aside] 214 | One really cool thing you can do with this technique is progressively stream pictures. Imagine seeing a blurry version of the whole image and slowly seeing it become more and more detailed as the download progresses and more DCT coefficients are available. This is actually possible to do with JPEG, but not as commonly used. 215 | [/aside] 216 | 217 | Just for fun, here's what it looks like using just [EditorLink editor:`refs.DCT3` pattern:"remove-{268}"]24,000 numbers[/EditorLink], or just [EditorLink editor:`refs.DCT3` pattern:"remove-{316}"]5000 numbers[/EditorLink]. Pretty blurry, but almost recognizable! 218 | 219 | ## 3. Run-Length, Delta & Huffman Encoding 220 | 221 | All the compression steps so far have been lossy. This last layer, by contrast, is lossless. It doesn't remove any information, but it does make the file size significantly smaller. 222 | 223 | How do you compress something without throwing away any information? Think about how you would represent a simple solid black image. 224 | 225 | [RawEditor ref:"Huffman1" fullWidth:true imageUrl:"static/images/black.jpg" showHeader:false isUrlExempt:true /] 226 | 227 | JPEG uses about 5,000 numbers to represent this, but we can do much better. Can you think of an encoding scheme to represent this image using as few bytes as possible? 228 | 229 | The smallest I could think of was four bytes: three to specify the color and one to specify how many pixels have this color. The idea of expressing all repeated values concisely this way is called **run-length encoding**. It's lossless because we can recover the encoded data exactly as it was before. 230 | 231 | The file size of the solid black JPEG image is much bigger than four bytes because remember that in the DCT layer, the compression is applied to 8x8 blocks at a time. So at minimum we'll need one DCT coefficient for each 64 pixel block. We only need one because instead of storing one DCT coefficient followed by 63 zeros for this image, run-length encoding allows us to just store one number and say "the rest are zero". 232 | 233 | **Delta-encoding** is the technique of storing each byte as a relative value compared to something before it instead of storing its absolute value. This is the reason editing certain bytes will change the color for all subsequent pixels. For example, instead of storing: 234 | 235 | ``` 236 | 12 13 14 14 14 13 13 14 237 | ``` 238 | 239 | You would start with 12, and from there, just store how much you need to add or subtract to get the next number. So once delta-encoded the sequence above becomes: 240 | 241 | ``` 242 | 12 1 1 0 0 -1 0 1 243 | ``` 244 | 245 | Once again, the transformed data is not any smaller than the original, but it is more compressible. Applying delta encoding before run-length can help a lot, while still remaining a completely lossless compression step. 246 | 247 | Delta encoding is one of the few techniques that is applied outside the 8x8 blocks. Out of the 64 DCT coefficients, the first one is just a constant wave function (you see it as a solid color). It represents the average brightness of each block for the luminance components, or the average blueness for the Cb components etc. This first value in each DCT block is called the DC value, and each DC value is delta-encoded relative to the ones before it. So changing the brightness of the very first block will affect all blocks in the image. 248 | 249 | This all leaves just one final mystery: how can changing just a single number completely wreck the image? This was not a property of any of compression layers so far. The answer lies in the JPEG header. It's the first 500 or so bytes that contain metadata about the image, like its width and height, and has been omitted from all the byte editors so far. 250 | 251 | Below is the original image with the header included. 252 | 253 | [RawEditor ref:"Huffman2" fullWidth:true imageUrl:"static/images/blanket-cat.jpg" maxWidth:700 /] 254 | 255 | Without the header, it's practically impossible (or at least very difficult) to decode the JPEG image. It would be as if I was trying to describe a painting to you, and I started to invent words to communicate what I saw. It's probably going to be a very concise description, since I can define the words to mean exactly what I want to communicate, but it would be meaningless to anyone other than me. 256 | 257 | This may sound ridiculous, but this is exactly what's going on here. Every single JPEG image is compressed with a code that's specific to this particular image. These codes are defined in a dictionary stored in the header. This technique is called **Huffman encoding**, and the dictionary is called a Huffman table. This table is marked in the header by two bytes: 255 followed by 196. Each color component may have its own Huffman table. 258 | 259 | Changes to these Huffman tables will have the most dramatic effects on any image. Changing [EditorLink editor:`refs.Huffman2` line:15 search:'125 1' replace:'125 12']the second 1 to 12 on line 15[/EditorLink] is a good example. Changing anything after the 125 on that line works too. 260 | 261 | The Huffman tables have such a dramatic effect on the image because they tell us how to read the individual bits. So far we've just been dealing with the binary numbers in decimal. This hides the fact that if you want to store the number [i]1[/i] in a byte, it would look like _00000001_, because each byte must have exactly eight bits even if it only needs one bit. 262 | 263 | This is potentially a huge waste of storage if you have a lot of small numbers. Huffman encoding is a technique that allows us to relax this requirement that each number must occupy eight bits. That means if you see the two bytes: 264 | 265 | ``` 266 | 234 115 267 | ``` 268 | 269 | Based on the Huffman table, these could actually be three values. To extract them, you'll need to first break them into their individual bits: 270 | 271 | ``` 272 | 11101010 01110011 273 | ``` 274 | 275 | [aside] 276 | One neat trick you can do with this knowledge is strip out the header from a JPEG image and save it separately. You're effectively making it so only you can read it. [a href:"https://code.fb.com/android/the-technology-behind-preview-photos/" target:"_blank"]Facebook actually does this[/a] to make JPEG images even smaller. 277 | 278 | Another thing you can do is change the Huffman table just slightly. To anyone else, it looks like a corrupted image. But only you would know the magic edit needed to fix it. 279 | [/aside] 280 | 281 | Then follow the table to figure out how to group them. For example, it could be the first six bits (111010) which is 58 in decimal, followed by another five bits (10011) which is 19 and finally the last four bits (0011), which is three. 282 | 283 | 284 | [div] 285 | 286 | This is why it's very difficult to make sense of the bytes at this layer of compression. The bytes don't actually represent what they seem to represent. I won't go into the details of how to extract the Huffman table and translate the bits in this article, but there are [Reference content:"Tom Scott's video on Huffman Encoding is a great explanation of how it works in general. This article by Calvin Hass at ImpulseAdventure goes into how to extract the Huffman tables from a JPEG image and use it for decoding."]many good resources on this if you're curious[/Reference]. 287 | 288 | [/div] 289 | 290 | 291 | [hr/] 292 | 293 | So to summarize, what all does it take to decode a JPEG image? You need to: 294 | 295 | 1. Extract the Huffman table(s) from the header and decode the bits. 296 | 2. Extract the Discrete Cosine Transform coefficients for each color/luminance component, for each 8x8 block, by undoing the run-length and delta encodings. 297 | 3. Combine the cosine waves based on the coefficients to get back the pixel values for each 8x8 block (this is known as the inverse Discrete Cosine Transform). 298 | 4. Scale up the chrominance components if they were subsampled (the header has this information). 299 | 5. Convert the resulting YCbCr of each pixel to RGB. 300 | 6. Display the image! 301 | 302 | That's a lot of work to view a simple cat picture! But what I love about this is that you can see how JPEG is a very human-centric technology. It relies on the quirks of our perception to achieve compression rates far greater than is possible with general purpose techniques. And now that you understand how JPEG works, you can imagine how many of these techniques can be extended to other domains. For example, applying delta-encoding in video can produce a huge file size reduction since there are often areas that don't change at all between frames (such as the background). 303 | 304 | All the code for this article is [a href:"https://github.com/ParametricPress/01-unraveling-the-jpeg"]open source[/a] and includes instructions on replacing the images in these byte editors with your own. 305 | 306 | [AuthorBio] 307 | [b][a href:"https://omarshehata.me/"]Omar Shehata[/a][/b] is a graphics programmer at Cesium working on open source, web-based 3D maps. He grew up in Alexandria, Egypt and currently lives in Philadelphia, PA. 308 | 309 | Edited by Matthew Conlen and Victoria Uren. 310 | [/AuthorBio] 311 | 312 | [NextArticle slug:parametricSlug fullWidth:true /] 313 | 314 | [Footer fullWidth:true /] 315 | 316 | 317 | [Initialize /] 318 | 319 | [Analytics google:"UA-139053456-1" tag:parametricSlug /] -------------------------------------------------------------------------------- /static/issue-01-theme.css: -------------------------------------------------------------------------------- 1 | 2 | @font-face { 3 | font-family: 'Silkscreen'; 4 | src: url('fonts/silkscreen/slkscr.ttf') format('truetype'); 5 | font-weight: 500; 6 | font-style: normal; 7 | } 8 | @font-face { 9 | font-family: 'Silkscreen'; 10 | src: url('fonts/silkscreen/slkscrb.ttf') format('truetype'); 11 | font-weight: 700; 12 | font-style: normal; 13 | } 14 | 15 | @font-face { 16 | font-family: 'Graphik Web'; 17 | src: url('fonts/graphik/Graphik-BlackItalic-Web.woff2') format('woff2'), 18 | url('fonts/graphik/Graphik-BlackItalic-Web.woff') format('woff'); 19 | font-weight: 800; 20 | font-style: italic; 21 | font-stretch: normal; 22 | } 23 | 24 | @font-face { 25 | font-family: 'Graphik Web'; 26 | src: url('fonts/graphik/Graphik-Black-Web.woff2') format('woff2'), 27 | url('fonts/graphik/Graphik-Black-Web.woff') format('woff'); 28 | font-weight: 800; 29 | font-style: normal; 30 | font-stretch: normal; 31 | } 32 | 33 | @font-face { 34 | font-family: 'Graphik Web'; 35 | src: url('fonts/graphik/Graphik-BoldItalic-Web.eot'); 36 | src: url('fonts/graphik/Graphik-BoldItalic-Web.eot?#iefix') format('embedded-opentype'), 37 | url('fonts/graphik/Graphik-BoldItalic-Web.woff2') format('woff2'), 38 | url('fonts/graphik/Graphik-BoldItalic-Web.woff') format('woff'); 39 | font-weight: 700; 40 | font-style: italic; 41 | font-stretch: normal; 42 | } 43 | 44 | @font-face { 45 | font-family: 'Graphik Web'; 46 | src: url('fonts/graphik/Graphik-Bold-Web.woff2') format('woff2'), 47 | url('fonts/graphik/Graphik-Bold-Web.woff') format('woff'); 48 | font-weight: 700; 49 | font-style: normal; 50 | font-stretch: normal; 51 | } 52 | 53 | @font-face { 54 | font-family: 'Graphik Web'; 55 | src: url('fonts/graphik/Graphik-MediumItalic-Web.woff2') format('woff2'), 56 | url('fonts/graphik/Graphik-MediumItalic-Web.woff') format('woff'); 57 | font-weight: 500; 58 | font-style: italic; 59 | font-stretch: normal; 60 | } 61 | 62 | @font-face { 63 | font-family: 'Graphik Web'; 64 | src: url('fonts/graphik/Graphik-Medium-Web.woff2') format('woff2'), 65 | url('fonts/graphik/Graphik-Medium-Web.woff') format('woff'); 66 | font-weight: 500; 67 | font-style: normal; 68 | font-stretch: normal; 69 | } 70 | 71 | @font-face { 72 | font-family: 'Graphik Web'; 73 | src: url('fonts/graphik/Graphik-RegularItalic-Web.woff2') format('woff2'), 74 | url('fonts/graphik/Graphik-RegularItalic-Web.woff') format('woff'); 75 | font-weight: 400; 76 | font-style: italic; 77 | font-stretch: normal; 78 | } 79 | 80 | @font-face { 81 | font-family: 'Graphik Web'; 82 | src: url('fonts/graphik/Graphik-Regular-Web.woff2') format('woff2'), 83 | url('fonts/graphik/Graphik-Regular-Web.woff') format('woff'); 84 | font-weight: 400; 85 | font-style: normal; 86 | font-stretch: normal; 87 | } 88 | 89 | 90 | @font-face { 91 | font-family: 'Bluu'; 92 | src: url('fonts/bluu/bluunext-bold-webfont.woff2') format('woff2'), 93 | url('fonts/bluu/bluunext-bold-webfont.woff') format('woff'), 94 | url('fonts/bluu/bluunext-bold.ttf') format('truetype'), 95 | url('fonts/bluu/bluunext-bold-webfont.svg#bluu_nextbold') format('svg'); 96 | font-weight: normal; 97 | font-style: normal; 98 | } 99 | 100 | @font-face { 101 | font-family: 'Bluu'; 102 | src: url('fonts/bluu/bluunext-bolditalic-webfont.woff2') format('woff2'), 103 | url('fonts/bluu/bluunext-bolditalic-webfont.woff') format('woff'), 104 | url('fonts/bluu/bluunext-bolditalic.ttf') format('truetype'), 105 | url('fonts/bluu/bluunext-bolditalic-webfont.svg#bluu_nextbold_italic') format('svg'); 106 | font-weight: normal; 107 | font-style: italic; 108 | } 109 | 110 | @font-face { 111 | font-family: 'Reforma'; 112 | src: url('fonts/reforma/Reforma1918-Blanca.ttf') format('truetype'); 113 | font-weight: 300; 114 | font-style: normal; 115 | } 116 | @font-face { 117 | font-family: 'Reforma'; 118 | src: url('fonts/reforma/Reforma1918-Negra.ttf') format('truetype'); 119 | font-weight: bold; 120 | font-style: normal; 121 | } 122 | @font-face { 123 | font-family: 'Reforma'; 124 | src: url('fonts/reforma/Reforma1918-Gris.ttf') format('truetype'); 125 | font-weight: normal; 126 | font-style: normal; 127 | } 128 | 129 | @font-face { 130 | font-family: 'Reforma'; 131 | src: url('fonts/reforma/Reforma1918-GrisItalica.ttf') format('truetype'); 132 | font-weight: normal; 133 | font-style: italic; 134 | } 135 | 136 | 137 | .ReactTable{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;border:1px solid rgba(0,0,0,0.1);}.ReactTable *{box-sizing:border-box}.ReactTable .rt-table{-webkit-box-flex:1;-ms-flex:1;flex:1;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;width:100%;border-collapse:collapse;overflow:auto}.ReactTable .rt-thead{-webkit-box-flex:1;-ms-flex:1 0 auto;flex:1 0 auto;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;}.ReactTable .rt-thead.-headerGroups{background:rgba(0,0,0,0.03);border-bottom:1px solid rgba(0,0,0,0.05)}.ReactTable .rt-thead.-filters{border-bottom:1px solid rgba(0,0,0,0.05);}.ReactTable .rt-thead.-filters .rt-th{border-right:1px solid rgba(0,0,0,0.02)}.ReactTable .rt-thead.-header{box-shadow:0 2px 15px 0 rgba(0,0,0,0.15)}.ReactTable .rt-thead .rt-tr{text-align:center}.ReactTable .rt-thead .rt-th,.ReactTable .rt-thead .rt-td{padding:5px 5px;line-height:normal;position:relative;border-right:1px solid rgba(0,0,0,0.05);-webkit-transition:box-shadow .3s cubic-bezier(.175,.885,.32,1.275);transition:box-shadow .3s cubic-bezier(.175,.885,.32,1.275);box-shadow:inset 0 0 0 0 transparent;}.ReactTable .rt-thead .rt-th.-sort-asc,.ReactTable .rt-thead .rt-td.-sort-asc{box-shadow:inset 0 3px 0 0 rgba(0,0,0,0.6)}.ReactTable .rt-thead .rt-th.-sort-desc,.ReactTable .rt-thead .rt-td.-sort-desc{box-shadow:inset 0 -3px 0 0 rgba(0,0,0,0.6)}.ReactTable .rt-thead .rt-th.-cursor-pointer,.ReactTable .rt-thead .rt-td.-cursor-pointer{cursor:pointer}.ReactTable .rt-thead .rt-th:last-child,.ReactTable .rt-thead .rt-td:last-child{border-right:0}.ReactTable .rt-thead .rt-resizable-header{overflow:visible;}.ReactTable .rt-thead .rt-resizable-header:last-child{overflow:hidden}.ReactTable .rt-thead .rt-resizable-header-content{overflow:hidden;text-overflow:ellipsis}.ReactTable .rt-thead .rt-header-pivot{border-right-color:#f7f7f7}.ReactTable .rt-thead .rt-header-pivot:after,.ReactTable .rt-thead .rt-header-pivot:before{left:100%;top:50%;border:solid transparent;content:" ";height:0;width:0;position:absolute;pointer-events:none}.ReactTable .rt-thead .rt-header-pivot:after{border-color:rgba(255,255,255,0);border-left-color:#fff;border-width:8px;margin-top:-8px}.ReactTable .rt-thead .rt-header-pivot:before{border-color:rgba(102,102,102,0);border-left-color:#f7f7f7;border-width:10px;margin-top:-10px}.ReactTable .rt-tbody{-webkit-box-flex:99999;-ms-flex:99999 1 auto;flex:99999 1 auto;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;overflow:auto;}.ReactTable .rt-tbody .rt-tr-group{border-bottom:solid 1px rgba(0,0,0,0.05);}.ReactTable .rt-tbody .rt-tr-group:last-child{border-bottom:0}.ReactTable .rt-tbody .rt-td{border-right:1px solid rgba(0,0,0,0.02);}.ReactTable .rt-tbody .rt-td:last-child{border-right:0}.ReactTable .rt-tbody .rt-expandable{cursor:pointer}.ReactTable .rt-tr-group{-webkit-box-flex:1;-ms-flex:1 0 auto;flex:1 0 auto;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch}.ReactTable .rt-tr{-webkit-box-flex:1;-ms-flex:1 0 auto;flex:1 0 auto;display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex}.ReactTable .rt-th,.ReactTable .rt-td{-webkit-box-flex:1;-ms-flex:1 0 0px;flex:1 0 0;white-space:nowrap;text-overflow:ellipsis;padding:7px 5px;overflow:hidden;-webkit-transition:.3s ease;transition:.3s ease;-webkit-transition-property:width,min-width,padding,opacity;transition-property:width,min-width,padding,opacity;}.ReactTable .rt-th.-hidden,.ReactTable .rt-td.-hidden{width:0 !important;min-width:0 !important;padding:0 !important;border:0 !important;opacity:0 !important}.ReactTable .rt-expander{display:inline-block;position:relative;margin:0;color:transparent;margin:0 10px;}.ReactTable .rt-expander:after{content:'';position:absolute;width:0;height:0;top:50%;left:50%;-webkit-transform:translate(-50%,-50%) rotate(-90deg);transform:translate(-50%,-50%) rotate(-90deg);border-left:5.04px solid transparent;border-right:5.04px solid transparent;border-top:7px solid rgba(0,0,0,0.8);-webkit-transition:all .3s cubic-bezier(.175,.885,.32,1.275);transition:all .3s cubic-bezier(.175,.885,.32,1.275);cursor:pointer}.ReactTable .rt-expander.-open:after{-webkit-transform:translate(-50%,-50%) rotate(0);transform:translate(-50%,-50%) rotate(0)}.ReactTable .rt-resizer{display:inline-block;position:absolute;width:36px;top:0;bottom:0;right:-18px;cursor:col-resize;z-index:10}.ReactTable .rt-tfoot{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;box-shadow:0 0 15px 0 rgba(0,0,0,0.15);}.ReactTable .rt-tfoot .rt-td{border-right:1px solid rgba(0,0,0,0.05);}.ReactTable .rt-tfoot .rt-td:last-child{border-right:0}.ReactTable.-striped .rt-tr.-odd{background:rgba(0,0,0,0.03)}.ReactTable.-highlight .rt-tbody .rt-tr:not(.-padRow):hover{background:rgba(0,0,0,0.05)}.ReactTable .-pagination{z-index:1;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;-ms-flex-wrap:wrap;flex-wrap:wrap;padding:3px;box-shadow:0 0 15px 0 rgba(0,0,0,0.1);border-top:2px solid rgba(0,0,0,0.1);}.ReactTable .-pagination .-btn{-webkit-appearance:none;-moz-appearance:none;appearance:none;display:block;width:100%;height:100%;border:0;border-radius:3px;padding:6px;font-size:1em;color:rgba(0,0,0,0.6);background:rgba(0,0,0,0.1);-webkit-transition:all .1s ease;transition:all .1s ease;cursor:pointer;outline:none;}.ReactTable .-pagination .-btn[disabled]{opacity:.5;cursor:default}.ReactTable .-pagination .-btn:not([disabled]):hover{background:rgba(0,0,0,0.3);color:#fff}.ReactTable .-pagination .-previous,.ReactTable .-pagination .-next{-webkit-box-flex:1;-ms-flex:1;flex:1;text-align:center}.ReactTable .-pagination .-center{-webkit-box-flex:1.5;-ms-flex:1.5;flex:1.5;text-align:center;margin-bottom:0;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-ms-flex-pack:distribute;justify-content:space-around}.ReactTable .-pagination .-pageInfo{display:inline-block;margin:3px 10px;white-space:nowrap}.ReactTable .-pagination .-pageJump{display:inline-block;}.ReactTable .-pagination .-pageJump input{width:70px;text-align:center}.ReactTable .-pagination .-pageSizeOptions{margin:3px 10px}.ReactTable .rt-noData{display:block;position:absolute;left:50%;top:50%;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);background:rgba(255,255,255,0.8);-webkit-transition:all .3s ease;transition:all .3s ease;z-index:1;pointer-events:none;padding:20px;color:rgba(0,0,0,0.5)}.ReactTable .-loading{display:block;position:absolute;left:0;right:0;top:0;bottom:0;background:rgba(255,255,255,0.8);-webkit-transition:all .3s ease;transition:all .3s ease;z-index:-1;opacity:0;pointer-events:none;}.ReactTable .-loading > div{position:absolute;display:block;text-align:center;width:100%;top:50%;left:0;font-size:15px;color:rgba(0,0,0,0.6);-webkit-transform:translateY(-52%);transform:translateY(-52%);-webkit-transition:all .3s cubic-bezier(.25,.46,.45,.94);transition:all .3s cubic-bezier(.25,.46,.45,.94)}.ReactTable .-loading.-active{opacity:1;z-index:2;pointer-events:all;}.ReactTable .-loading.-active > div{-webkit-transform:translateY(50%);transform:translateY(50%)}.ReactTable input,.ReactTable select{border:1px solid rgba(0,0,0,0.1);background:#fff;padding:5px 7px;font-size:inherit;border-radius:3px;font-weight:normal;outline:none}.ReactTable .rt-resizing .rt-th,.ReactTable .rt-resizing .rt-td{-webkit-transition:none !important;transition:none !important;cursor:col-resize;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none} 138 | 139 | * { 140 | box-sizing: border-box; 141 | } 142 | body { 143 | -ms-text-size-adjust: 100%; 144 | -webkit-text-size-adjust: 100%; 145 | line-height: 1.5; 146 | color: #24292e; 147 | font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 148 | font-size: 16px; 149 | line-height: 1.5; 150 | word-wrap: break-word; 151 | } 152 | 153 | .pl-c { 154 | color: #969896; 155 | } 156 | 157 | .pl-c1, 158 | .pl-s .pl-v { 159 | color: #0086b3; 160 | } 161 | 162 | .pl-e, 163 | .pl-en { 164 | color: #795da3; 165 | } 166 | 167 | .pl-smi, 168 | .pl-s .pl-s1 { 169 | color: #333; 170 | } 171 | 172 | .pl-ent { 173 | color: #63a35c; 174 | } 175 | 176 | .pl-k { 177 | color: #a71d5d; 178 | } 179 | 180 | .pl-s, 181 | .pl-pds, 182 | .pl-s .pl-pse .pl-s1, 183 | .pl-sr, 184 | .pl-sr .pl-cce, 185 | .pl-sr .pl-sre, 186 | .pl-sr .pl-sra { 187 | color: #183691; 188 | } 189 | 190 | .pl-v, 191 | .pl-smw { 192 | color: #ed6a43; 193 | } 194 | 195 | .pl-bu { 196 | color: #b52a1d; 197 | } 198 | 199 | .pl-ii { 200 | color: #f8f8f8; 201 | background-color: #b52a1d; 202 | } 203 | 204 | .pl-c2 { 205 | color: #f8f8f8; 206 | background-color: #b52a1d; 207 | } 208 | 209 | .pl-c2::before { 210 | content: "\\000d"; 211 | } 212 | 213 | .pl-sr .pl-cce { 214 | font-weight: bold; 215 | color: #63a35c; 216 | } 217 | 218 | .pl-ml { 219 | color: #693a17; 220 | } 221 | 222 | .pl-mh, 223 | .pl-mh .pl-en, 224 | .pl-ms { 225 | font-weight: bold; 226 | color: #1d3e81; 227 | } 228 | 229 | .pl-mq { 230 | color: #008080; 231 | } 232 | 233 | .pl-mi { 234 | font-style: italic; 235 | color: #333; 236 | } 237 | 238 | .pl-mb { 239 | font-weight: bold; 240 | color: #333; 241 | } 242 | 243 | .pl-md { 244 | color: #bd2c00; 245 | background-color: #ffecec; 246 | } 247 | 248 | .pl-mi1 { 249 | color: #55a532; 250 | background-color: #eaffea; 251 | } 252 | 253 | .pl-mc { 254 | color: #ef9700; 255 | background-color: #ffe3b4; 256 | } 257 | 258 | .pl-mi2 { 259 | color: #d8d8d8; 260 | background-color: #808080; 261 | } 262 | 263 | .pl-mdr { 264 | font-weight: bold; 265 | color: #795da3; 266 | } 267 | 268 | .pl-mo { 269 | color: #1d3e81; 270 | } 271 | 272 | .pl-ba { 273 | color: #595e62; 274 | } 275 | 276 | .pl-sg { 277 | color: #c0c0c0; 278 | } 279 | 280 | .pl-corl { 281 | text-decoration: underline; 282 | color: #183691; 283 | } 284 | 285 | .octicon { 286 | display: inline-block; 287 | vertical-align: text-top; 288 | fill: currentColor; 289 | } 290 | 291 | a { 292 | background-color: transparent; 293 | -webkit-text-decoration-skip: objects; 294 | } 295 | 296 | a:active, 297 | a:hover { 298 | outline-width: 0; 299 | } 300 | 301 | strong { 302 | font-weight: inherit; 303 | } 304 | 305 | strong { 306 | font-weight: bolder; 307 | } 308 | 309 | h1 { 310 | font-size: 2em; 311 | margin: 0.67em 0; 312 | } 313 | 314 | img { 315 | border-style: none; 316 | } 317 | 318 | svg:not(:root) { 319 | overflow: hidden; 320 | } 321 | 322 | code, 323 | kbd, 324 | pre { 325 | font-family: monospace, monospace; 326 | font-size: 1em; 327 | } 328 | 329 | hr { 330 | box-sizing: content-box; 331 | height: 0; 332 | overflow: visible; 333 | } 334 | 335 | input { 336 | font: inherit; 337 | margin: 10px 10px 20px 0; 338 | } 339 | 340 | input { 341 | overflow: visible; 342 | } 343 | 344 | [type="checkbox"] { 345 | box-sizing: border-box; 346 | padding: 0; 347 | } 348 | 349 | 350 | input { 351 | font-family: inherit; 352 | font-size: inherit; 353 | line-height: inherit; 354 | } 355 | 356 | a { 357 | color: #0366d6; 358 | text-decoration: none; 359 | } 360 | 361 | a:hover { 362 | text-decoration: underline; 363 | } 364 | 365 | strong { 366 | font-weight: 600; 367 | } 368 | 369 | table { 370 | border-spacing: 0; 371 | border-collapse: collapse; 372 | } 373 | 374 | td, 375 | th { 376 | padding: 0; 377 | } 378 | 379 | h1, 380 | h2, 381 | h3, 382 | h4, 383 | h5, 384 | h6 { 385 | margin-top: 0; 386 | margin-bottom: 0; 387 | } 388 | 389 | h1 { 390 | font-size: 32px; 391 | font-weight: 600; 392 | } 393 | 394 | h2 { 395 | font-size: 24px; 396 | font-weight: 600; 397 | } 398 | 399 | h3 { 400 | font-size: 20px; 401 | font-weight: 600; 402 | } 403 | 404 | h4 { 405 | font-size: 16px; 406 | font-weight: 600; 407 | } 408 | 409 | h5 { 410 | font-size: 14px; 411 | font-weight: 600; 412 | } 413 | 414 | h6 { 415 | font-size: 12px; 416 | font-weight: 600; 417 | } 418 | 419 | p { 420 | margin-top: 0; 421 | margin-bottom: 10px; 422 | } 423 | 424 | blockquote { 425 | margin: 0; 426 | } 427 | 428 | ul, 429 | ol { 430 | padding-left: 0; 431 | margin-top: 0; 432 | margin-bottom: 0; 433 | } 434 | 435 | ol ol, 436 | ul ol { 437 | list-style-type: lower-roman; 438 | } 439 | 440 | ul ul ol, 441 | ul ol ol, 442 | ol ul ol, 443 | ol ol ol { 444 | list-style-type: lower-alpha; 445 | } 446 | 447 | dd { 448 | margin-left: 0; 449 | } 450 | 451 | code { 452 | font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; 453 | font-size: 12px; 454 | } 455 | 456 | pre { 457 | margin-top: 0; 458 | margin-bottom: 0; 459 | font: 12px "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; 460 | } 461 | 462 | .octicon { 463 | vertical-align: text-bottom; 464 | } 465 | 466 | .pl-0 { 467 | padding-left: 0 !important; 468 | } 469 | 470 | .pl-1 { 471 | padding-left: 4px !important; 472 | } 473 | 474 | .pl-2 { 475 | padding-left: 8px !important; 476 | } 477 | 478 | .pl-3 { 479 | padding-left: 16px !important; 480 | } 481 | 482 | .pl-4 { 483 | padding-left: 24px !important; 484 | } 485 | 486 | .pl-5 { 487 | padding-left: 32px !important; 488 | } 489 | 490 | .pl-6 { 491 | padding-left: 40px !important; 492 | } 493 | 494 | .idyll-root { 495 | max-width: 1440px; 496 | /* margin-left: 0; */ 497 | } 498 | .idyll-root::before { 499 | display: table; 500 | content: ""; 501 | } 502 | 503 | .idyll-root::after { 504 | display: table; 505 | clear: both; 506 | content: ""; 507 | } 508 | 509 | .idyll-root>*:first-child { 510 | margin-top: 0 !important; 511 | } 512 | 513 | .idyll-root>*:last-child { 514 | margin-bottom: 0 !important; 515 | } 516 | 517 | a:not([href]) { 518 | color: inherit; 519 | text-decoration: none; 520 | } 521 | 522 | .anchor { 523 | float: left; 524 | padding-right: 4px; 525 | margin-left: -20px; 526 | line-height: 1; 527 | } 528 | 529 | .anchor:focus { 530 | outline: none; 531 | } 532 | 533 | p, 534 | blockquote, 535 | ul, 536 | ol, 537 | dl, 538 | table, 539 | pre { 540 | margin-top: 0; 541 | margin-bottom: 16px; 542 | } 543 | 544 | hr { 545 | height: 2px; 546 | padding: 0; 547 | margin: 2em 0; 548 | background-color: #222; 549 | border: 0; 550 | } 551 | 552 | blockquote { 553 | padding: 0 1em; 554 | color: #6a737d; 555 | border-left: 0.25em solid #dfe2e5; 556 | } 557 | 558 | blockquote>:first-child { 559 | margin-top: 0; 560 | } 561 | 562 | blockquote>:last-child { 563 | margin-bottom: 0; 564 | } 565 | 566 | kbd { 567 | display: inline-block; 568 | padding: 3px 5px; 569 | font-size: 11px; 570 | line-height: 10px; 571 | color: #444d56; 572 | vertical-align: middle; 573 | background-color: #fafbfc; 574 | border: solid 1px #c6cbd1; 575 | border-bottom-color: #959da5; 576 | border-radius: 3px; 577 | box-shadow: inset 0 -1px 0 #959da5; 578 | } 579 | 580 | h1, 581 | h2, 582 | h3, 583 | h4, 584 | h5, 585 | h6 { 586 | margin-top: 2em; 587 | margin-bottom: 0.5em; 588 | font-weight: 600; 589 | line-height: 1.25; 590 | } 591 | 592 | h1 .octicon-link, 593 | h2 .octicon-link, 594 | h3 .octicon-link, 595 | h4 .octicon-link, 596 | h5 .octicon-link, 597 | h6 .octicon-link { 598 | color: #1b1f23; 599 | vertical-align: middle; 600 | visibility: hidden; 601 | } 602 | 603 | h1:hover .anchor, 604 | h2:hover .anchor, 605 | h3:hover .anchor, 606 | h4:hover .anchor, 607 | h5:hover .anchor, 608 | h6:hover .anchor { 609 | text-decoration: none; 610 | } 611 | 612 | h1:hover .anchor .octicon-link, 613 | h2:hover .anchor .octicon-link, 614 | h3:hover .anchor .octicon-link, 615 | h4:hover .anchor .octicon-link, 616 | h5:hover .anchor .octicon-link, 617 | h6:hover .anchor .octicon-link { 618 | visibility: visible; 619 | } 620 | 621 | h1 { 622 | padding-bottom: 0.3em; 623 | font-size: 2em; 624 | border-bottom: 1px solid #eaecef; 625 | } 626 | 627 | h2 { 628 | padding-bottom: 0.3em; 629 | font-size: 1.5em; 630 | border-bottom: 1px solid #eaecef; 631 | } 632 | 633 | h3 { 634 | font-size: 1.25em; 635 | } 636 | 637 | h4 { 638 | font-size: 1em; 639 | } 640 | 641 | h5 { 642 | font-size: 0.875em; 643 | } 644 | 645 | h6 { 646 | font-size: 0.85em; 647 | color: #6a737d; 648 | } 649 | 650 | h1.hed, 651 | h2.dek { 652 | border-bottom: none; 653 | padding-bottom: 0; 654 | margin-top: 12px; 655 | } 656 | 657 | ul, 658 | ol { 659 | padding-left: 2em; 660 | } 661 | 662 | ul ul, 663 | ul ol, 664 | ol ol, 665 | ol ul { 666 | margin-top: 0; 667 | margin-bottom: 0; 668 | } 669 | 670 | li>p { 671 | margin-top: 16px; 672 | } 673 | 674 | li+li { 675 | margin-top: 0.25em; 676 | } 677 | 678 | dl { 679 | padding: 0; 680 | } 681 | 682 | dl dt { 683 | padding: 0; 684 | margin-top: 16px; 685 | font-size: 1em; 686 | font-style: italic; 687 | font-weight: 600; 688 | } 689 | 690 | dl dd { 691 | padding: 0 16px; 692 | margin-bottom: 16px; 693 | } 694 | 695 | table { 696 | display: block; 697 | width: 100%; 698 | overflow: auto; 699 | } 700 | 701 | table th { 702 | font-weight: 600; 703 | } 704 | 705 | table th, 706 | table td { 707 | padding: 6px 13px; 708 | border: 1px solid #dfe2e5; 709 | } 710 | 711 | table tr { 712 | background-color: #fff; 713 | border-top: 1px solid #c6cbd1; 714 | } 715 | 716 | table tr:nth-child(2n) { 717 | background-color: #f6f8fa; 718 | } 719 | 720 | img { 721 | /* max-width: 100%; */ 722 | box-sizing: content-box; 723 | } 724 | 725 | code { 726 | padding: 0; 727 | padding-top: 0.2em; 728 | padding-bottom: 0.2em; 729 | margin: 0; 730 | font-size: 85%; 731 | background-color: rgba(27,31,35,0.05); 732 | border-radius: 3px; 733 | } 734 | 735 | code::before, 736 | code::after { 737 | letter-spacing: -0.2em; 738 | content: "\\00a0"; 739 | } 740 | 741 | pre { 742 | word-wrap: normal; 743 | } 744 | 745 | pre>code { 746 | padding: 0; 747 | margin: 0; 748 | font-size: 100%; 749 | word-break: normal; 750 | white-space: pre; 751 | background: transparent; 752 | border: 0; 753 | } 754 | 755 | .highlight { 756 | margin-bottom: 16px; 757 | } 758 | 759 | .highlight pre { 760 | margin-bottom: 0; 761 | word-break: normal; 762 | } 763 | 764 | .highlight pre, 765 | pre { 766 | padding: 16px; 767 | overflow: auto; 768 | font-size: 85%; 769 | line-height: 1.45; 770 | background-color: #f6f8fa; 771 | border-radius: 3px; 772 | } 773 | 774 | pre code { 775 | display: inline; 776 | max-width: auto; 777 | padding: 0; 778 | margin: 0; 779 | overflow: visible; 780 | line-height: inherit; 781 | word-wrap: normal; 782 | background-color: transparent; 783 | border: 0; 784 | } 785 | 786 | pre code::before, 787 | pre code::after { 788 | content: normal; 789 | } 790 | 791 | .full-commit .btn-outline:not(:disabled):hover { 792 | color: #005cc5; 793 | border-color: #005cc5; 794 | } 795 | 796 | kbd { 797 | display: inline-block; 798 | padding: 3px 5px; 799 | font: 11px "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; 800 | line-height: 10px; 801 | color: #444d56; 802 | vertical-align: middle; 803 | background-color: #fcfcfc; 804 | border: solid 1px #c6cbd1; 805 | border-bottom-color: #959da5; 806 | border-radius: 3px; 807 | box-shadow: inset 0 -1px 0 #959da5; 808 | } 809 | 810 | :checked+.radio-label { 811 | position: relative; 812 | z-index: 1; 813 | border-color: #0366d6; 814 | } 815 | 816 | .task-list-item { 817 | list-style-type: none; 818 | } 819 | 820 | .task-list-item+.task-list-item { 821 | margin-top: 3px; 822 | } 823 | 824 | .task-list-item input { 825 | margin: 0 0.2em 0.25em -1.6em; 826 | vertical-align: middle; 827 | } 828 | 829 | .idyll-dynamic { 830 | text-decoration: underline; 831 | text-decoration-style: dotted; 832 | } 833 | 834 | .idyll-action { 835 | text-decoration: underline; 836 | } 837 | 838 | .idyll-document-error { 839 | color: red; 840 | font-family: monospace; 841 | } 842 | 843 | 844 | 845 | .idyll-step-graphic { 846 | top: 0; 847 | left: 0; 848 | right: 0; 849 | bottom: 0; 850 | position: absolute; 851 | height: 100%; 852 | overflow: hidden; 853 | margin: 0 auto; 854 | text-align: center; 855 | display: flex; 856 | justify-content: center; 857 | align-items: center; 858 | background: black; 859 | } 860 | 861 | .idyll-scroll-graphic { 862 | 863 | text-align: center; 864 | width: 100%; 865 | } 866 | 867 | .idyll-step-graphic img { 868 | flex-shrink: 0; 869 | min-width: 100%; 870 | min-height: 100% 871 | } 872 | 873 | .idyll-step-content { 874 | left: 0; 875 | right: 0; 876 | bottom: 0; 877 | position: absolute; 878 | color: white; 879 | padding: 10px; 880 | background: rgba(0, 0, 0, 0.8); 881 | } 882 | 883 | .idyll-stepper-control { 884 | position: absolute; 885 | top: 50%; 886 | transform: translateY(-50%); 887 | width: 100%; 888 | } 889 | 890 | .idyll-stepper-control-button { 891 | background: rgba(0, 0, 0, 0.7); 892 | color: white; 893 | font-weight: bold; 894 | padding: 15px 10px; 895 | cursor: pointer; 896 | } 897 | 898 | .idyll-stepper-control-button-previous { 899 | position: absolute; 900 | left: 10px; 901 | } 902 | 903 | .idyll-stepper-control-button-next { 904 | position: absolute; 905 | right: 10px; 906 | } 907 | 908 | .idyll-stepper { 909 | margin: 60px 0; 910 | } 911 | 912 | .idyll-scroll { 913 | margin-top: 25vh; 914 | } 915 | 916 | .idyll-scroll-text { 917 | padding: 50vh 0; 918 | } 919 | 920 | .idyll-scroll-text .idyll-step { 921 | margin: 75vh 0 75vh 0; 922 | padding: 50px; 923 | background: white; 924 | } 925 | 926 | 927 | 928 | body { 929 | /* Before the wildfire: */ 930 | font-family: Reforma; 931 | font-size: 20px; 932 | color: #222222; 933 | line-height: 40px; 934 | font-weight: 500; 935 | /* opacity: 0; 936 | transition: opacity 0.5s; */ 937 | } 938 | 939 | .hed { 940 | /* Parametric: */ 941 | font-family: bluu; 942 | font-size: 124px; 943 | /* width: 700px; */ 944 | line-height: 111px; 945 | } 946 | 947 | .hed .press { 948 | /* Press: */ 949 | font-family: bluu; 950 | font-style: italic; 951 | } 952 | 953 | .dek { 954 | /* Call for Proposals: */ 955 | font-family: 'Graphik Web'; 956 | font-size: 20px; 957 | } 958 | 959 | .dek .date { 960 | /* Fall/Winter 2018: */ 961 | font-family: bluu; 962 | font-style: italic; 963 | font-size: 22px; 964 | } 965 | 966 | h1, h2, h3, h4, h5, h6 { 967 | /* font-size: 20px; */ 968 | font-family: 'Graphik Web'; 969 | text-decoration: none; 970 | border-bottom: none; 971 | } 972 | 973 | h1.hed, h2.dek { 974 | margin: 0; 975 | } 976 | 977 | h2.dek { 978 | margin-top: 48px; 979 | } 980 | 981 | h2 { 982 | margin: 2em 0 0.5em; 983 | } 984 | 985 | .idyll-root { 986 | margin-bottom: 0 !important; 987 | padding-bottom: 0; 988 | padding-top: 0; 989 | } 990 | 991 | 992 | button, input[type=submit] { 993 | display: block; 994 | margin: 1em auto; 995 | background: #4801FF; 996 | font-family: Silkscreen; 997 | border: none; 998 | box-shadow: none; 999 | border-radius: 0px; 1000 | color: white; 1001 | font-size: 14px; 1002 | padding: 1em 2em; 1003 | cursor: pointer; 1004 | -webkit-appearance: none; 1005 | -moz-appearance: none; 1006 | appearance: none; 1007 | } 1008 | 1009 | 1010 | pre { 1011 | max-width: 960px; 1012 | margin: 2em auto; 1013 | } 1014 | 1015 | h1.hed { 1016 | font-size: 4em; 1017 | margin-top: 0; 1018 | } 1019 | 1020 | h2.dek { 1021 | font-size: 2em; 1022 | margin: 0.5em auto; 1023 | max-width: 800px; 1024 | font-weight: lighter; 1025 | } 1026 | 1027 | .article-header { 1028 | /* text-align: center; */ 1029 | color: #2D2D2D; 1030 | padding: -0.5em 0 -0.5em 0; 1031 | margin-bottom: 4em; 1032 | padding-left: 0; 1033 | } 1034 | 1035 | .article-header .hed-container { 1036 | color: #2D2D2D; 1037 | background: #2D2D2D; 1038 | width: 100%; 1039 | text-shadow: 1040 | -0.5px -0.5px 0 #B0B0B0, 1041 | 0.5px -0.5px 0 #B0B0B0, 1042 | -0.5px 0.5px 0 #B0B0B0, 1043 | 0.5px 0.5px 0 #B0B0B0; 1044 | /* white-space: nowrap; */ 1045 | /* display: flex; */ 1046 | /* justify-content: center; */ 1047 | /* overflow: hidden; */ 1048 | } 1049 | .article-header .header-highlight { 1050 | color: #fff; 1051 | text-shadow: none; 1052 | margin-left: 0.25em; 1053 | margin-right: 0.25em; 1054 | font-size: 1.2em; 1055 | } 1056 | 1057 | .article-header a { 1058 | 1059 | text-decoration: underline; 1060 | 1061 | } 1062 | .idyll-dynamic { 1063 | cursor: ew-resize; 1064 | font-family: monospace; 1065 | } 1066 | .idyll-display { 1067 | font-family: monospace; 1068 | } 1069 | 1070 | .dynamic-module { 1071 | background: #efefef; 1072 | margin: 2em -2em; 1073 | padding: 1em; 1074 | border-radius: 10px; 1075 | } 1076 | 1077 | img { 1078 | display: block; 1079 | margin: 0 auto; 1080 | max-width: 100%; 1081 | } 1082 | 1083 | .parametric-metadata-container { 1084 | position: relative; 1085 | } 1086 | 1087 | 1088 | .parametric-metadata { 1089 | position: absolute; 1090 | left: 2em; 1091 | font-size: 20px; 1092 | font-family: 'Reforma'; 1093 | font-weight: 300; 1094 | letter-spacing: -0.78px; 1095 | } 1096 | 1097 | .parametric-metadata-header { 1098 | font-size: 20px; 1099 | font-family: 'Graphik Web'; 1100 | font-weight: bold; 1101 | } 1102 | .parametric-metadata-role { 1103 | font-family: 'Bluu'; 1104 | /* font-weight: bold; */ 1105 | } 1106 | 1107 | .idyll-text-container { 1108 | max-width: 720px; 1109 | } 1110 | 1111 | .article-header { 1112 | width: 100%; 1113 | max-width: none; 1114 | } 1115 | .article-header img { 1116 | max-width: none; 1117 | } 1118 | 1119 | .idyll-text-container { 1120 | max-width: 720px; 1121 | } 1122 | 1123 | .article-header { 1124 | width: 100%; 1125 | max-width: none; 1126 | margin-bottom: 2em; 1127 | } 1128 | 1129 | .article-header .hed-container { 1130 | background: none; 1131 | } 1132 | 1133 | h1.hed { 1134 | opacity: 0.1; 1135 | font-family: "Graphik Web"; 1136 | font-weight: 800; 1137 | font-size: 240px; 1138 | color: #4801FF; 1139 | line-height: 200px; 1140 | position: absolute; 1141 | width: 1400px; 1142 | } 1143 | h1.hed-offset { 1144 | position: relative; 1145 | top: -8px; 1146 | left: -13px; 1147 | pointer-events: none; 1148 | } 1149 | 1150 | h1.hed-rotate { 1151 | font-family: "Graphik Web"; 1152 | font-weight: 800; 1153 | font-size: 240px; 1154 | margin-top: 0; 1155 | color: #222222; 1156 | /* Safari */ 1157 | line-height: 200px; 1158 | pointer-events: none; 1159 | user-select: none; 1160 | 1161 | -webkit-transform: rotate(90deg); 1162 | -webkit-transform-origin: left top; 1163 | -moz-transform: rotate(90deg); 1164 | -moz-transform-origin: left top; 1165 | -ms-transform: rotate(90deg); 1166 | -ms-transform-origin: left top; 1167 | -o-transform: rotate(90deg); 1168 | -o-transform-origin: left top; 1169 | transform: rotate(90deg); 1170 | transform-origin: left top; 1171 | 1172 | position: absolute; 1173 | top: -75px; 1174 | left: calc(100% - 40px); 1175 | white-space: nowrap; 1176 | } 1177 | 1178 | body { 1179 | overflow-x: hidden; 1180 | } 1181 | 1182 | .aside { 1183 | font-family: 'Graphik Web'; 1184 | font-size: 14px; 1185 | line-height: 16px; 1186 | font-weight: 400; 1187 | } 1188 | 1189 | a { 1190 | color: #4801FF; 1191 | } 1192 | 1193 | .parametric-inset { 1194 | margin: 2em 2em; 1195 | font-family: 'Graphik Web'; 1196 | } 1197 | 1198 | .parametric-article-dek { 1199 | font-size: 32px; 1200 | line-height: 1.5; 1201 | } 1202 | 1203 | .parametric-article-nav { 1204 | margin-top: 25px; 1205 | margin-left: 50px; 1206 | margin-right: 50px; 1207 | z-index: 1000; 1208 | font-size: 32px; 1209 | } 1210 | 1211 | .parametric-header-text { 1212 | display: flex; 1213 | width: 100vw; 1214 | max-width: 1440px; 1215 | position: relative; 1216 | top: 4em; 1217 | } 1218 | 1219 | .parametric-header-hed { 1220 | height: 520px; 1221 | } 1222 | 1223 | .parametric-header-hed-text { 1224 | position: relative; 1225 | top: 60px; 1226 | font-family: 'Graphik Web'; 1227 | margin-left: 0; 1228 | } 1229 | 1230 | .parametric-recirc-next-article { 1231 | width: 100%; 1232 | line-height: 1.1; 1233 | font-weight: bold; 1234 | margin-bottom: 1em; 1235 | margin-left: 50px; 1236 | max-width: 1340px; 1237 | } 1238 | .parametric-recirc-next-article a { 1239 | font-family: 'Graphik Web'; 1240 | font-size: 32px; 1241 | color: #ffffff; 1242 | text-shadow: -1px -1px 0 #4801FF, 1px -1px 0 #4801FF, -1px 1px 0 #4801FF, 1px 1px 0 #4801FF; 1243 | transition: all 0.25s; 1244 | cursor: pointer; 1245 | } 1246 | 1247 | .parametric-recirc-next-article a:hover .highlight-hover { 1248 | color: #4801FF; 1249 | /* text-shadow: none; */ 1250 | } 1251 | 1252 | 1253 | 1254 | .parametric-issue-toc { 1255 | font-size: 28px; 1256 | } 1257 | 1258 | .parametric-float-right { 1259 | max-width: 1000px; 1260 | margin: 2em 2em 2em auto; 1261 | } 1262 | 1263 | .parametric-caption { 1264 | font-family: 'Graphik Web'; 1265 | line-height: 1.25; 1266 | /* padding-top: 0.5em; */ 1267 | margin-top: 1em; 1268 | /* border-top: solid 1px #c5c5c5; */ 1269 | margin-bottom: 2em; 1270 | /* max-width: 600px; */ 1271 | font-size: 14px; 1272 | line-height: 1.25; 1273 | font-weight: 400; 1274 | } 1275 | 1276 | .parametric-caption a { 1277 | color: #24292e; 1278 | text-decoration: underline; 1279 | } 1280 | 1281 | .parametric-tooltip-trigger { 1282 | cursor: pointer; 1283 | text-decoration: underline; 1284 | } 1285 | .parametric-tooltip-trigger:hover { 1286 | color: #4801FF; 1287 | } 1288 | 1289 | .parametric-tooltip { 1290 | background-color: #fff !important; 1291 | color: #222 !important; 1292 | box-shadow: 2px 2px 5px #222; 1293 | font-family: 'Graphik Web'; 1294 | font-size: 14px !important; 1295 | max-width: 400px; 1296 | line-height: 1.25; 1297 | opacity: 1 !important; 1298 | border-radius: 0 !important; 1299 | } 1300 | 1301 | .parametric-tooltip:after { 1302 | border: none !important; 1303 | } 1304 | 1305 | .parametric-long-title { 1306 | font-weight: 900; 1307 | font-family: 'Graphik Web'; 1308 | font-size: 56px; 1309 | line-height: 64px; 1310 | color: #222; 1311 | } 1312 | 1313 | .parametric-header-image img { 1314 | width: 400px; 1315 | z-index: 3; 1316 | position: relative; 1317 | top: -70px; 1318 | left: -120px; 1319 | background: none; 1320 | } 1321 | 1322 | .hed-rotate { 1323 | z-index: 10; 1324 | pointer-events: none; 1325 | } 1326 | 1327 | .parametric-footer { 1328 | background: #222222; 1329 | color: white; 1330 | padding: 2em 50px 3em 50px; 1331 | margin-top: 2em; 1332 | font-family: 'Graphik Web'; 1333 | font-size: 16px; 1334 | width: 100vw; 1335 | left: 0; 1336 | position: absolute; 1337 | } 1338 | 1339 | .parametric-footer-link-container { 1340 | display: flex; 1341 | flex-direction: row; 1342 | } 1343 | .parametric-footer-link-container > div { 1344 | margin-right: 1em; 1345 | } 1346 | 1347 | .parametric-footer form { 1348 | text-align: right; 1349 | display: flex; 1350 | flex-direction: row; 1351 | margin-left: 2em; 1352 | line-height: 1.75; 1353 | font-size: 12px; 1354 | font-family: 'Graphik Web'; 1355 | height: 100%; 1356 | } 1357 | 1358 | .parametric-footer > div { 1359 | max-width: 1340px; 1360 | margin: 0 auto; 1361 | } 1362 | .parametric-footer img { 1363 | width: 300px; 1364 | margin: 0; 1365 | margin-bottom: 1em; 1366 | } 1367 | .parametric-footer input { 1368 | margin: 0; 1369 | } 1370 | 1371 | .parametric-footer form label { 1372 | font-size: 23px; 1373 | } 1374 | 1375 | .parametric-footer input[type=text] { 1376 | width: 200px; 1377 | } 1378 | .parametric-footer input[type=submit] { 1379 | padding: 0; 1380 | margin: 0; 1381 | margin-left: 1em; 1382 | width: 100px; 1383 | } 1384 | .parametric-footer a { 1385 | color: white; 1386 | } 1387 | 1388 | .mobile { 1389 | display: none; 1390 | } 1391 | 1392 | .parametric-author-bio a { 1393 | color: #222; 1394 | text-decoration: underline; 1395 | } 1396 | 1397 | @media all and (max-width: 1200px) { 1398 | .parametric-metadata-container { 1399 | position: 1400 | max-width: 720px; 1401 | margin: 0 auto; 1402 | } 1403 | 1404 | .parametric-metadata { 1405 | position: 1406 | display: flex; 1407 | flex-direction: row; 1408 | justify-content: space-around 1409 | } 1410 | /* h1.hed-rotate { 1411 | font-size: 96px; 1412 | line-height: 100px; 1413 | } */ 1414 | h1.hed-rotate { 1415 | display: none; 1416 | } 1417 | } 1418 | 1419 | @media all and (max-width: 1000px) { 1420 | .idyll-root { 1421 | max-width: 100vw; 1422 | padding: 0; 1423 | } 1424 | 1425 | .desktop { 1426 | display: none; 1427 | } 1428 | .mobile { 1429 | display: initial; 1430 | } 1431 | 1432 | .parametric-recirc-module { 1433 | max-width: 350px; 1434 | margin: 2em auto; 1435 | } 1436 | .parametric-recirc-next-article { 1437 | margin-left: 0; 1438 | margin-right: 0; 1439 | padding-left: 1em; 1440 | padding-right: 1em; 1441 | } 1442 | .parametric-recirc-next-article a { 1443 | font-size: 24px; 1444 | } 1445 | /* h1.hed { 1446 | font-size: 96px; 1447 | line-height: 100px; 1448 | } */ 1449 | 1450 | .parametric-long-title { 1451 | max-width: calc(100vw - 1em); 1452 | font-size: 36px; 1453 | line-height: 1.1 1454 | } 1455 | 1456 | 1457 | .idyll-text-container img { 1458 | margin: 2em -1em; 1459 | width: calc(100% + 2em); 1460 | max-width: none; 1461 | } 1462 | 1463 | .parametric-header-hed-text { 1464 | max-width: 90vw; 1465 | margin-left: 0; 1466 | } 1467 | 1468 | .parametric-footer { 1469 | flex-direction: column; 1470 | padding: 1em 1em 3em 1em; 1471 | } 1472 | 1473 | .parametric-footer form { 1474 | text-align: left; 1475 | margin-top: 1em; 1476 | } 1477 | .parametric-footer form label { 1478 | font-size: 18px; 1479 | } 1480 | 1481 | .aside-container { 1482 | margin-bottom: 2em; 1483 | margin-top: 2em; 1484 | } 1485 | 1486 | .parametric-inset { 1487 | margin: 2em 0; 1488 | } 1489 | 1490 | .parametric-footer-link-container { 1491 | flex-direction: column; 1492 | } 1493 | .parametric-footer-link-container > div { 1494 | margin-right: 0; 1495 | } 1496 | 1497 | .parametric-footer form { 1498 | margin: 1em 0; 1499 | } 1500 | 1501 | .parametric-footer form { 1502 | flex-direction: column; 1503 | width: calc(100vw - 2em); 1504 | max-width: 100%; 1505 | } 1506 | .parametric-footer input[type=submit] { 1507 | display: block; 1508 | width: 100%; 1509 | margin: 1em 0; 1510 | padding: 1em 0; 1511 | } 1512 | .parametric-footer input[type=text] { 1513 | width: 100%; 1514 | } 1515 | .parametric-issue-toc { 1516 | font-size: 22px; 1517 | } 1518 | .parametric-nav-toc { 1519 | text-align: right; 1520 | } 1521 | ul, ol { 1522 | padding-left: 1em; 1523 | } 1524 | } 1525 | 1526 | @media all and (max-width: 800px) { 1527 | 1528 | body { 1529 | font-size: 16px; 1530 | line-height: 32px; 1531 | } 1532 | .article-header { 1533 | overflow: hidden; 1534 | } 1535 | 1536 | .parametric-header-image { 1537 | display: none; 1538 | } 1539 | 1540 | .parametric-article-dek { 1541 | font-size: 24px; 1542 | } 1543 | .parametric-article-nav { 1544 | margin-left: 1em; 1545 | margin-right: 1em; 1546 | font-size: 24px; 1547 | line-height: 32px; 1548 | } 1549 | 1550 | .parametric-nav-links { 1551 | font-size: 18px; 1552 | line-height: 1.5; 1553 | } 1554 | 1555 | .parametric-header-text { 1556 | top: 0; 1557 | } 1558 | .parametric-header-hed { 1559 | height: 400px; 1560 | } 1561 | .idyll-root { 1562 | max-width: 100vw; 1563 | padding: 0; 1564 | } 1565 | 1566 | .parametric-header-hed-text { 1567 | top: 60px; 1568 | } 1569 | 1570 | } 1571 | 1572 | @media all and (max-width: 320px) { 1573 | .parametric-issue-toc { 1574 | font-size: 18px; 1575 | line-height: 24px; 1576 | } 1577 | } 1578 | 1579 | --------------------------------------------------------------------------------