├── .github ├── FUNDING.yml └── workflows │ └── test.yml ├── .gitignore ├── LICENSE.txt ├── README.md ├── bin └── sync-vendor-libs.sh ├── css ├── demo.css └── vendor │ ├── Jcrop.gif │ └── jquery.Jcrop.css ├── docker-compose.yml ├── index.html ├── js ├── demo │ └── demo.js ├── index.js ├── load-image-exif-map.js ├── load-image-exif.js ├── load-image-fetch.js ├── load-image-iptc-map.js ├── load-image-iptc.js ├── load-image-meta.js ├── load-image-orientation.js ├── load-image-scale.js ├── load-image.all.min.js ├── load-image.all.min.js.map ├── load-image.js └── vendor │ ├── canvas-to-blob.js │ ├── jquery.Jcrop.js │ ├── jquery.js │ └── promise-polyfill.js ├── package-lock.json ├── package.json └── test ├── index.html ├── test.js └── vendor ├── chai.js ├── mocha.css └── mocha.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [blueimp] 2 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | node-version: [14, 16] 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-node@v2 14 | with: 15 | node-version: ${{ matrix.node-version }} 16 | - run: npm install 17 | - run: npm run lint 18 | 19 | unit: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: mocha 24 | run: docker-compose run --rm mocha 25 | - name: docker-compose logs 26 | if: always() 27 | run: docker-compose logs nginx 28 | - name: docker-compose down 29 | if: always() 30 | run: docker-compose down -v 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright © 2011 Sebastian Tschan, https://blueimp.net 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /bin/sync-vendor-libs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd "$(dirname "$0")/.." 3 | cp node_modules/blueimp-canvas-to-blob/js/canvas-to-blob.js js/vendor/ 4 | cp node_modules/jquery/dist/jquery.js js/vendor/ 5 | cp node_modules/promise-polyfill/dist/polyfill.js js/vendor/promise-polyfill.js 6 | cp node_modules/chai/chai.js test/vendor/ 7 | cp node_modules/mocha/mocha.js test/vendor/ 8 | cp node_modules/mocha/mocha.css test/vendor/ 9 | -------------------------------------------------------------------------------- /css/demo.css: -------------------------------------------------------------------------------- 1 | /* 2 | * JavaScript Load Image Demo CSS 3 | * https://github.com/blueimp/JavaScript-Load-Image 4 | * 5 | * Copyright 2013, Sebastian Tschan 6 | * https://blueimp.net 7 | * 8 | * Licensed under the MIT license: 9 | * https://opensource.org/licenses/MIT 10 | */ 11 | 12 | body { 13 | max-width: 990px; 14 | margin: 0 auto; 15 | padding: 1em; 16 | font-family: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', 17 | Arial, sans-serif; 18 | -webkit-text-size-adjust: 100%; 19 | line-height: 1.4; 20 | background: #212121; 21 | color: #dedede; 22 | } 23 | a { 24 | color: #61afef; 25 | text-decoration: none; 26 | } 27 | a:visited { 28 | color: #56b6c2; 29 | } 30 | a:hover { 31 | color: #98c379; 32 | } 33 | table { 34 | width: 100%; 35 | word-wrap: break-word; 36 | table-layout: fixed; 37 | border-collapse: collapse; 38 | } 39 | figure { 40 | margin: 0; 41 | padding: 0.75em; 42 | border-radius: 5px; 43 | display: inline-block; 44 | } 45 | table, 46 | figure { 47 | margin-bottom: 1.25em; 48 | } 49 | tr, 50 | figure { 51 | background: #363636; 52 | } 53 | tr:nth-child(odd) { 54 | background: #414141; 55 | } 56 | td, 57 | th { 58 | padding: 0.5em 0.75em; 59 | text-align: left; 60 | } 61 | img, 62 | canvas { 63 | max-width: 100%; 64 | border: 0; 65 | vertical-align: middle; 66 | } 67 | h1, 68 | h2, 69 | h3, 70 | h4, 71 | h5, 72 | h6 { 73 | margin-top: 1.5em; 74 | margin-bottom: 0.5em; 75 | } 76 | h1 { 77 | margin-top: 0.5em; 78 | } 79 | label { 80 | display: inline-block; 81 | margin-bottom: 0.25em; 82 | } 83 | button, 84 | select, 85 | input, 86 | textarea { 87 | -webkit-appearance: none; 88 | box-sizing: border-box; 89 | margin: 0; 90 | padding: 0.5em 0.75em; 91 | font-family: inherit; 92 | font-size: 100%; 93 | line-height: 1.4; 94 | background: #414141; 95 | color: #dedede; 96 | border: 1px solid #363636; 97 | border-radius: 5px; 98 | box-shadow: 0 0 4px rgba(0, 0, 0, 0.07); 99 | } 100 | input, 101 | textarea { 102 | width: 100%; 103 | box-shadow: inset 0 0 4px rgba(0, 0, 0, 0.07); 104 | } 105 | textarea { 106 | display: block; 107 | overflow: auto; 108 | } 109 | button { 110 | background: #3c76a7; 111 | background: linear-gradient(180deg, #3c76a7, #225c8d); 112 | border-color: #225c8d; 113 | color: #fff; 114 | } 115 | button[type='submit'] { 116 | background: #6fa349; 117 | background: linear-gradient(180deg, #6fa349, #568a30); 118 | border-color: #568a30; 119 | } 120 | button[type='reset'] { 121 | background: #d79435; 122 | background: linear-gradient(180deg, #d79435, #be7b1c); 123 | border-color: #be7b1c; 124 | } 125 | select { 126 | display: block; 127 | padding-right: 2.25em; 128 | background: #3c76a7; 129 | background: url('data:image/svg+xml;charset=utf8,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 4 5"%3E%3Cpath fill="%23fff" d="M2 0L0 2h4zm0 5L0 3h4z"/%3E%3C/svg%3E') 130 | no-repeat right 0.75em center/0.75em, 131 | linear-gradient(180deg, #3c76a7, #225c8d); 132 | border-color: #225c8d; 133 | color: #fff; 134 | } 135 | button:active, 136 | select:active { 137 | box-shadow: inset 0 0 8px rgba(0, 0, 0, 0.5); 138 | } 139 | select::-ms-expand { 140 | display: none; 141 | } 142 | option { 143 | color: #212121; 144 | } 145 | input[type='checkbox'] { 146 | -webkit-appearance: checkbox; 147 | width: auto; 148 | padding: initial; 149 | box-shadow: none; 150 | } 151 | input[type='file'] { 152 | max-width: 100%; 153 | padding: 0; 154 | background: none; 155 | border: 0; 156 | border-radius: 0; 157 | box-shadow: none; 158 | } 159 | 160 | input[type='file']::-webkit-file-upload-button { 161 | -webkit-appearance: none; 162 | box-sizing: border-box; 163 | padding: 0.5em 0.75em; 164 | font-family: inherit; 165 | font-size: 100%; 166 | line-height: 1.4; 167 | background: linear-gradient(180deg, #3c76a7, #225c8d); 168 | border: 1px solid #225c8d; 169 | color: #fff; 170 | border-radius: 5px; 171 | box-shadow: 0 0 4px rgba(0, 0, 0, 0.07); 172 | } 173 | input[type='file']::-webkit-file-upload-button:active { 174 | box-shadow: inset 0 0 8px rgba(0, 0, 0, 0.5); 175 | } 176 | input[type='file']::-ms-browse { 177 | box-sizing: border-box; 178 | padding: 0.5em 0.75em; 179 | font-family: inherit; 180 | font-size: 100%; 181 | line-height: 1.4; 182 | background: linear-gradient(180deg, #3c76a7, #225c8d); 183 | border: 1px solid #225c8d; 184 | color: #fff; 185 | border-radius: 5px; 186 | box-shadow: 0 0 4px rgba(0, 0, 0, 0.07); 187 | } 188 | input[type='file']::-ms-browse:active { 189 | box-shadow: inset 0 0 8px rgba(0, 0, 0, 0.5); 190 | } 191 | 192 | @media (prefers-color-scheme: light) { 193 | body { 194 | background: #ececec; 195 | color: #212121; 196 | } 197 | a { 198 | color: #225c8d; 199 | } 200 | a:visited { 201 | color: #378f9a; 202 | } 203 | a:hover { 204 | color: #6fa349; 205 | } 206 | figure, 207 | tr { 208 | background: #fff; 209 | color: #212121; 210 | } 211 | tr:nth-child(odd) { 212 | background: #f6f6f6; 213 | } 214 | input, 215 | textarea { 216 | background: #fff; 217 | border-color: #d1d1d1; 218 | color: #212121; 219 | } 220 | } 221 | 222 | #result { 223 | display: block; 224 | } 225 | 226 | @media (min-width: 540px) { 227 | #navigation { 228 | list-style: none; 229 | padding: 0; 230 | } 231 | #navigation li { 232 | display: inline-block; 233 | } 234 | #navigation li:not(:first-child)::before { 235 | content: ' | '; 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /css/vendor/Jcrop.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blueimp/JavaScript-Load-Image/5d34ed4ce3472ecb22d920c66cc4d7604526ff24/css/vendor/Jcrop.gif -------------------------------------------------------------------------------- /css/vendor/jquery.Jcrop.css: -------------------------------------------------------------------------------- 1 | /* jquery.Jcrop.css v0.9.12 - MIT License */ 2 | /* 3 | The outer-most container in a typical Jcrop instance 4 | If you are having difficulty with formatting related to styles 5 | on a parent element, place any fixes here or in a like selector 6 | 7 | You can also style this element if you want to add a border, etc 8 | A better method for styling can be seen below with .jcrop-light 9 | (Add a class to the holder and style elements for that extended class) 10 | */ 11 | .jcrop-holder { 12 | direction: ltr; 13 | text-align: left; 14 | } 15 | /* Selection Border */ 16 | .jcrop-vline, 17 | .jcrop-hline { 18 | background: #ffffff url("Jcrop.gif"); 19 | font-size: 0; 20 | position: absolute; 21 | } 22 | .jcrop-vline { 23 | height: 100%; 24 | width: 1px !important; 25 | } 26 | .jcrop-vline.right { 27 | right: 0; 28 | } 29 | .jcrop-hline { 30 | height: 1px !important; 31 | width: 100%; 32 | } 33 | .jcrop-hline.bottom { 34 | bottom: 0; 35 | } 36 | /* Invisible click targets */ 37 | .jcrop-tracker { 38 | height: 100%; 39 | width: 100%; 40 | /* "turn off" link highlight */ 41 | -webkit-tap-highlight-color: transparent; 42 | /* disable callout, image save panel */ 43 | -webkit-touch-callout: none; 44 | /* disable cut copy paste */ 45 | -webkit-user-select: none; 46 | } 47 | /* Selection Handles */ 48 | .jcrop-handle { 49 | background-color: #333333; 50 | border: 1px #eeeeee solid; 51 | width: 7px; 52 | height: 7px; 53 | font-size: 1px; 54 | } 55 | .jcrop-handle.ord-n { 56 | left: 50%; 57 | margin-left: -4px; 58 | margin-top: -4px; 59 | top: 0; 60 | } 61 | .jcrop-handle.ord-s { 62 | bottom: 0; 63 | left: 50%; 64 | margin-bottom: -4px; 65 | margin-left: -4px; 66 | } 67 | .jcrop-handle.ord-e { 68 | margin-right: -4px; 69 | margin-top: -4px; 70 | right: 0; 71 | top: 50%; 72 | } 73 | .jcrop-handle.ord-w { 74 | left: 0; 75 | margin-left: -4px; 76 | margin-top: -4px; 77 | top: 50%; 78 | } 79 | .jcrop-handle.ord-nw { 80 | left: 0; 81 | margin-left: -4px; 82 | margin-top: -4px; 83 | top: 0; 84 | } 85 | .jcrop-handle.ord-ne { 86 | margin-right: -4px; 87 | margin-top: -4px; 88 | right: 0; 89 | top: 0; 90 | } 91 | .jcrop-handle.ord-se { 92 | bottom: 0; 93 | margin-bottom: -4px; 94 | margin-right: -4px; 95 | right: 0; 96 | } 97 | .jcrop-handle.ord-sw { 98 | bottom: 0; 99 | left: 0; 100 | margin-bottom: -4px; 101 | margin-left: -4px; 102 | } 103 | /* Dragbars */ 104 | .jcrop-dragbar.ord-n, 105 | .jcrop-dragbar.ord-s { 106 | height: 7px; 107 | width: 100%; 108 | } 109 | .jcrop-dragbar.ord-e, 110 | .jcrop-dragbar.ord-w { 111 | height: 100%; 112 | width: 7px; 113 | } 114 | .jcrop-dragbar.ord-n { 115 | margin-top: -4px; 116 | } 117 | .jcrop-dragbar.ord-s { 118 | bottom: 0; 119 | margin-bottom: -4px; 120 | } 121 | .jcrop-dragbar.ord-e { 122 | margin-right: -4px; 123 | right: 0; 124 | } 125 | .jcrop-dragbar.ord-w { 126 | margin-left: -4px; 127 | } 128 | /* The "jcrop-light" class/extension */ 129 | .jcrop-light .jcrop-vline, 130 | .jcrop-light .jcrop-hline { 131 | background: #ffffff; 132 | filter: alpha(opacity=70) !important; 133 | opacity: .70!important; 134 | } 135 | .jcrop-light .jcrop-handle { 136 | -moz-border-radius: 3px; 137 | -webkit-border-radius: 3px; 138 | background-color: #000000; 139 | border-color: #ffffff; 140 | border-radius: 3px; 141 | } 142 | /* The "jcrop-dark" class/extension */ 143 | .jcrop-dark .jcrop-vline, 144 | .jcrop-dark .jcrop-hline { 145 | background: #000000; 146 | filter: alpha(opacity=70) !important; 147 | opacity: 0.7 !important; 148 | } 149 | .jcrop-dark .jcrop-handle { 150 | -moz-border-radius: 3px; 151 | -webkit-border-radius: 3px; 152 | background-color: #ffffff; 153 | border-color: #000000; 154 | border-radius: 3px; 155 | } 156 | /* Simple macro to turn off the antlines */ 157 | .solid-line .jcrop-vline, 158 | .solid-line .jcrop-hline { 159 | background: #ffffff; 160 | } 161 | /* Fix for twitter bootstrap et al. */ 162 | .jcrop-holder img, 163 | img.jcrop-preview { 164 | max-width: none; 165 | } 166 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | services: 3 | nginx: 4 | image: nginx:alpine 5 | ports: 6 | - 127.0.0.1:80:80 7 | volumes: 8 | - .:/usr/share/nginx/html:ro 9 | mocha: 10 | image: blueimp/mocha-chrome 11 | command: http://nginx/test 12 | environment: 13 | - WAIT_FOR_HOSTS=nginx:80 14 | depends_on: 15 | - nginx 16 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | 19 | 20 | JavaScript Load Image 21 | 25 | 26 | 29 | 30 | 31 | 39 | 40 | 41 |

JavaScript Load Image Demo

42 |

43 | JavaScript Load Image 46 | is a library to load images provided as 47 | File 48 | or 49 | Blob 50 | objects or via URL.
51 | It returns an optionally scaled, 52 | cropped or rotated HTML 53 | img 56 | or 57 | canvas 60 | element. 61 |

62 |

63 | It also provides methods to parse image metadata to extract 64 | IPTC and 65 | Exif tags as well as 66 | embedded thumbnail images, to overwrite the Exif Orientation value and to 67 | restore the complete image header after resizing. 68 |

69 | 89 |

File input

90 |

91 | 92 | 93 |

94 |

95 | 96 | 97 |

98 |

Or drag & drop an image file onto this webpage.

99 |

Options

100 |

101 | 102 | 114 |

115 |

116 | 117 | 118 |

119 |

Result

120 | 125 |
126 |
127 | Loading images from File objects requires support for the 128 | URL 129 | or 130 | FileReader 133 | API. 134 |
135 |
136 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 154 | 155 | 156 | 157 | 158 | 159 | -------------------------------------------------------------------------------- /js/demo/demo.js: -------------------------------------------------------------------------------- 1 | /* 2 | * JavaScript Load Image Demo JS 3 | * https://github.com/blueimp/JavaScript-Load-Image 4 | * 5 | * Copyright 2013, Sebastian Tschan 6 | * https://blueimp.net 7 | * 8 | * Licensed under the MIT license: 9 | * https://opensource.org/licenses/MIT 10 | */ 11 | 12 | /* global loadImage, $ */ 13 | 14 | $(function () { 15 | 'use strict' 16 | 17 | var resultNode = $('#result') 18 | var metaNode = $('#meta') 19 | var thumbNode = $('#thumbnail') 20 | var actionsNode = $('#actions') 21 | var orientationNode = $('#orientation') 22 | var imageSmoothingNode = $('#image-smoothing') 23 | var fileInputNode = $('#file-input') 24 | var urlNode = $('#url') 25 | var editNode = $('#edit') 26 | var cropNode = $('#crop') 27 | var cancelNode = $('#cancel') 28 | var coordinates 29 | var jcropAPI 30 | 31 | /** 32 | * Displays tag data 33 | * 34 | * @param {*} node jQuery node 35 | * @param {object} tags Tags map 36 | * @param {string} title Tags title 37 | */ 38 | function displayTagData(node, tags, title) { 39 | var table = $('
') 40 | var row = $('') 41 | var cell = $('') 42 | var headerCell = $('') 43 | var prop 44 | table.append(row.clone().append(headerCell.clone().text(title))) 45 | for (prop in tags) { 46 | if (Object.prototype.hasOwnProperty.call(tags, prop)) { 47 | if (typeof tags[prop] === 'object') { 48 | displayTagData(node, tags[prop], prop) 49 | continue 50 | } 51 | table.append( 52 | row 53 | .clone() 54 | .append(cell.clone().text(prop)) 55 | .append(cell.clone().text(tags[prop])) 56 | ) 57 | } 58 | } 59 | node.append(table).show() 60 | } 61 | 62 | /** 63 | * Displays the thumbnal image 64 | * 65 | * @param {*} node jQuery node 66 | * @param {string} thumbnail Thumbnail URL 67 | * @param {object} [options] Options object 68 | */ 69 | function displayThumbnailImage(node, thumbnail, options) { 70 | if (thumbnail) { 71 | var link = $('') 72 | .attr('href', loadImage.createObjectURL(thumbnail)) 73 | .attr('download', 'thumbnail.jpg') 74 | .appendTo(node) 75 | loadImage( 76 | thumbnail, 77 | function (img) { 78 | link.append(img) 79 | node.show() 80 | }, 81 | options 82 | ) 83 | } 84 | } 85 | 86 | /** 87 | * Displays metadata 88 | * 89 | * @param {object} [data] Metadata object 90 | */ 91 | function displayMetaData(data) { 92 | if (!data) return 93 | metaNode.data(data) 94 | var exif = data.exif 95 | var iptc = data.iptc 96 | if (exif) { 97 | var thumbnail = exif.get('Thumbnail') 98 | if (thumbnail) { 99 | displayThumbnailImage(thumbNode, thumbnail.get('Blob'), { 100 | orientation: exif.get('Orientation') 101 | }) 102 | } 103 | displayTagData(metaNode, exif.getAll(), 'TIFF') 104 | } 105 | if (iptc) { 106 | displayTagData(metaNode, iptc.getAll(), 'IPTC') 107 | } 108 | } 109 | 110 | /** 111 | * Removes meta data from the page 112 | */ 113 | function removeMetaData() { 114 | metaNode.hide().removeData().find('table').remove() 115 | thumbNode.hide().empty() 116 | } 117 | 118 | /** 119 | * Updates the results view 120 | * 121 | * @param {*} img Image or canvas element 122 | * @param {object} [data] Metadata object 123 | * @param {boolean} [keepMetaData] Keep meta data if true 124 | */ 125 | function updateResults(img, data, keepMetaData) { 126 | var isCanvas = window.HTMLCanvasElement && img instanceof HTMLCanvasElement 127 | if (!keepMetaData) { 128 | removeMetaData() 129 | if (data) { 130 | displayMetaData(data) 131 | } 132 | if (isCanvas) { 133 | actionsNode.show() 134 | } else { 135 | actionsNode.hide() 136 | } 137 | } 138 | if (!(isCanvas || img.src)) { 139 | resultNode 140 | .children() 141 | .replaceWith($('Loading image file failed')) 142 | return 143 | } 144 | var content = $('').append(img) 145 | resultNode.children().replaceWith(content) 146 | if (data.imageHead) { 147 | if (data.exif) { 148 | // Reset Exif Orientation data: 149 | loadImage.writeExifData(data.imageHead, data, 'Orientation', 1) 150 | } 151 | img.toBlob(function (blob) { 152 | if (!blob) return 153 | loadImage.replaceHead(blob, data.imageHead, function (newBlob) { 154 | content 155 | .attr('href', loadImage.createObjectURL(newBlob)) 156 | .attr('download', 'image.jpg') 157 | }) 158 | }, 'image/jpeg') 159 | } 160 | } 161 | 162 | /** 163 | * Displays the image 164 | * 165 | * @param {File|Blob|string} file File or Blob object or image URL 166 | */ 167 | function displayImage(file) { 168 | var options = { 169 | maxWidth: resultNode.width(), 170 | canvas: true, 171 | pixelRatio: window.devicePixelRatio, 172 | downsamplingRatio: 0.5, 173 | orientation: Number(orientationNode.val()) || true, 174 | imageSmoothingEnabled: imageSmoothingNode.is(':checked'), 175 | meta: true 176 | } 177 | if (!loadImage(file, updateResults, options)) { 178 | removeMetaData() 179 | resultNode 180 | .children() 181 | .replaceWith( 182 | $( 183 | '' + 184 | 'Your browser does not support the URL or FileReader API.' + 185 | '' 186 | ) 187 | ) 188 | } 189 | } 190 | 191 | /** 192 | * Handles drop and file selection change events 193 | * 194 | * @param {event} event Drop or file selection change event 195 | */ 196 | function fileChangeHandler(event) { 197 | event.preventDefault() 198 | var originalEvent = event.originalEvent 199 | var target = originalEvent.dataTransfer || originalEvent.target 200 | var file = target && target.files && target.files[0] 201 | if (!file) { 202 | return 203 | } 204 | displayImage(file) 205 | } 206 | 207 | /** 208 | * Handles URL change events 209 | */ 210 | function urlChangeHandler() { 211 | var url = $(this).val() 212 | if (url) displayImage(url) 213 | } 214 | 215 | // Show the URL/FileReader API requirement message if not supported: 216 | if ( 217 | window.createObjectURL || 218 | window.URL || 219 | window.webkitURL || 220 | window.FileReader 221 | ) { 222 | resultNode.children().hide() 223 | } else { 224 | resultNode.children().show() 225 | } 226 | 227 | $(document) 228 | .on('dragover', function (e) { 229 | e.preventDefault() 230 | if (event.dataTransfer) event.dataTransfer.dropEffect = 'copy' 231 | }) 232 | .on('drop', fileChangeHandler) 233 | 234 | fileInputNode.on('change', fileChangeHandler) 235 | 236 | urlNode.on('change paste input', urlChangeHandler) 237 | 238 | orientationNode.on('change', function () { 239 | var img = resultNode.find('img, canvas')[0] 240 | if (img) { 241 | updateResults( 242 | loadImage.scale(img, { 243 | maxWidth: resultNode.width() * (window.devicePixelRatio || 1), 244 | pixelRatio: window.devicePixelRatio, 245 | orientation: Number(orientationNode.val()) || true, 246 | imageSmoothingEnabled: imageSmoothingNode.is(':checked') 247 | }), 248 | metaNode.data(), 249 | true 250 | ) 251 | } 252 | }) 253 | 254 | editNode.on('click', function (event) { 255 | event.preventDefault() 256 | var imgNode = resultNode.find('img, canvas') 257 | var img = imgNode[0] 258 | var pixelRatio = window.devicePixelRatio || 1 259 | var margin = img.width / pixelRatio >= 140 ? 40 : 0 260 | imgNode 261 | // eslint-disable-next-line new-cap 262 | .Jcrop( 263 | { 264 | setSelect: [ 265 | margin, 266 | margin, 267 | img.width / pixelRatio - margin, 268 | img.height / pixelRatio - margin 269 | ], 270 | onSelect: function (coords) { 271 | coordinates = coords 272 | }, 273 | onRelease: function () { 274 | coordinates = null 275 | } 276 | }, 277 | function () { 278 | jcropAPI = this 279 | } 280 | ) 281 | .parent() 282 | .on('click', function (event) { 283 | event.preventDefault() 284 | }) 285 | }) 286 | 287 | cropNode.on('click', function (event) { 288 | event.preventDefault() 289 | var img = resultNode.find('img, canvas')[0] 290 | var pixelRatio = window.devicePixelRatio || 1 291 | if (img && coordinates) { 292 | updateResults( 293 | loadImage.scale(img, { 294 | left: coordinates.x * pixelRatio, 295 | top: coordinates.y * pixelRatio, 296 | sourceWidth: coordinates.w * pixelRatio, 297 | sourceHeight: coordinates.h * pixelRatio, 298 | maxWidth: resultNode.width() * pixelRatio, 299 | contain: true, 300 | pixelRatio: pixelRatio, 301 | imageSmoothingEnabled: imageSmoothingNode.is(':checked') 302 | }), 303 | metaNode.data(), 304 | true 305 | ) 306 | coordinates = null 307 | } 308 | }) 309 | 310 | cancelNode.on('click', function (event) { 311 | event.preventDefault() 312 | if (jcropAPI) { 313 | jcropAPI.release() 314 | jcropAPI.disable() 315 | } 316 | }) 317 | }) 318 | -------------------------------------------------------------------------------- /js/index.js: -------------------------------------------------------------------------------- 1 | /* global module, require */ 2 | 3 | module.exports = require('./load-image') 4 | 5 | require('./load-image-scale') 6 | require('./load-image-meta') 7 | require('./load-image-fetch') 8 | require('./load-image-exif') 9 | require('./load-image-exif-map') 10 | require('./load-image-iptc') 11 | require('./load-image-iptc-map') 12 | require('./load-image-orientation') 13 | -------------------------------------------------------------------------------- /js/load-image-exif-map.js: -------------------------------------------------------------------------------- 1 | /* 2 | * JavaScript Load Image Exif Map 3 | * https://github.com/blueimp/JavaScript-Load-Image 4 | * 5 | * Copyright 2013, Sebastian Tschan 6 | * https://blueimp.net 7 | * 8 | * Exif tags mapping based on 9 | * https://github.com/jseidelin/exif-js 10 | * 11 | * Licensed under the MIT license: 12 | * https://opensource.org/licenses/MIT 13 | */ 14 | 15 | /* global define, module, require */ 16 | 17 | ;(function (factory) { 18 | 'use strict' 19 | if (typeof define === 'function' && define.amd) { 20 | // Register as an anonymous AMD module: 21 | define(['./load-image', './load-image-exif'], factory) 22 | } else if (typeof module === 'object' && module.exports) { 23 | factory(require('./load-image'), require('./load-image-exif')) 24 | } else { 25 | // Browser globals: 26 | factory(window.loadImage) 27 | } 28 | })(function (loadImage) { 29 | 'use strict' 30 | 31 | var ExifMapProto = loadImage.ExifMap.prototype 32 | 33 | ExifMapProto.tags = { 34 | // ================= 35 | // TIFF tags (IFD0): 36 | // ================= 37 | 0x0100: 'ImageWidth', 38 | 0x0101: 'ImageHeight', 39 | 0x0102: 'BitsPerSample', 40 | 0x0103: 'Compression', 41 | 0x0106: 'PhotometricInterpretation', 42 | 0x0112: 'Orientation', 43 | 0x0115: 'SamplesPerPixel', 44 | 0x011c: 'PlanarConfiguration', 45 | 0x0212: 'YCbCrSubSampling', 46 | 0x0213: 'YCbCrPositioning', 47 | 0x011a: 'XResolution', 48 | 0x011b: 'YResolution', 49 | 0x0128: 'ResolutionUnit', 50 | 0x0111: 'StripOffsets', 51 | 0x0116: 'RowsPerStrip', 52 | 0x0117: 'StripByteCounts', 53 | 0x0201: 'JPEGInterchangeFormat', 54 | 0x0202: 'JPEGInterchangeFormatLength', 55 | 0x012d: 'TransferFunction', 56 | 0x013e: 'WhitePoint', 57 | 0x013f: 'PrimaryChromaticities', 58 | 0x0211: 'YCbCrCoefficients', 59 | 0x0214: 'ReferenceBlackWhite', 60 | 0x0132: 'DateTime', 61 | 0x010e: 'ImageDescription', 62 | 0x010f: 'Make', 63 | 0x0110: 'Model', 64 | 0x0131: 'Software', 65 | 0x013b: 'Artist', 66 | 0x8298: 'Copyright', 67 | 0x8769: { 68 | // ExifIFDPointer 69 | 0x9000: 'ExifVersion', // EXIF version 70 | 0xa000: 'FlashpixVersion', // Flashpix format version 71 | 0xa001: 'ColorSpace', // Color space information tag 72 | 0xa002: 'PixelXDimension', // Valid width of meaningful image 73 | 0xa003: 'PixelYDimension', // Valid height of meaningful image 74 | 0xa500: 'Gamma', 75 | 0x9101: 'ComponentsConfiguration', // Information about channels 76 | 0x9102: 'CompressedBitsPerPixel', // Compressed bits per pixel 77 | 0x927c: 'MakerNote', // Any desired information written by the manufacturer 78 | 0x9286: 'UserComment', // Comments by user 79 | 0xa004: 'RelatedSoundFile', // Name of related sound file 80 | 0x9003: 'DateTimeOriginal', // Date and time when the original image was generated 81 | 0x9004: 'DateTimeDigitized', // Date and time when the image was stored digitally 82 | 0x9010: 'OffsetTime', // Time zone when the image file was last changed 83 | 0x9011: 'OffsetTimeOriginal', // Time zone when the image was stored digitally 84 | 0x9012: 'OffsetTimeDigitized', // Time zone when the image was stored digitally 85 | 0x9290: 'SubSecTime', // Fractions of seconds for DateTime 86 | 0x9291: 'SubSecTimeOriginal', // Fractions of seconds for DateTimeOriginal 87 | 0x9292: 'SubSecTimeDigitized', // Fractions of seconds for DateTimeDigitized 88 | 0x829a: 'ExposureTime', // Exposure time (in seconds) 89 | 0x829d: 'FNumber', 90 | 0x8822: 'ExposureProgram', // Exposure program 91 | 0x8824: 'SpectralSensitivity', // Spectral sensitivity 92 | 0x8827: 'PhotographicSensitivity', // EXIF 2.3, ISOSpeedRatings in EXIF 2.2 93 | 0x8828: 'OECF', // Optoelectric conversion factor 94 | 0x8830: 'SensitivityType', 95 | 0x8831: 'StandardOutputSensitivity', 96 | 0x8832: 'RecommendedExposureIndex', 97 | 0x8833: 'ISOSpeed', 98 | 0x8834: 'ISOSpeedLatitudeyyy', 99 | 0x8835: 'ISOSpeedLatitudezzz', 100 | 0x9201: 'ShutterSpeedValue', // Shutter speed 101 | 0x9202: 'ApertureValue', // Lens aperture 102 | 0x9203: 'BrightnessValue', // Value of brightness 103 | 0x9204: 'ExposureBias', // Exposure bias 104 | 0x9205: 'MaxApertureValue', // Smallest F number of lens 105 | 0x9206: 'SubjectDistance', // Distance to subject in meters 106 | 0x9207: 'MeteringMode', // Metering mode 107 | 0x9208: 'LightSource', // Kind of light source 108 | 0x9209: 'Flash', // Flash status 109 | 0x9214: 'SubjectArea', // Location and area of main subject 110 | 0x920a: 'FocalLength', // Focal length of the lens in mm 111 | 0xa20b: 'FlashEnergy', // Strobe energy in BCPS 112 | 0xa20c: 'SpatialFrequencyResponse', 113 | 0xa20e: 'FocalPlaneXResolution', // Number of pixels in width direction per FPRUnit 114 | 0xa20f: 'FocalPlaneYResolution', // Number of pixels in height direction per FPRUnit 115 | 0xa210: 'FocalPlaneResolutionUnit', // Unit for measuring the focal plane resolution 116 | 0xa214: 'SubjectLocation', // Location of subject in image 117 | 0xa215: 'ExposureIndex', // Exposure index selected on camera 118 | 0xa217: 'SensingMethod', // Image sensor type 119 | 0xa300: 'FileSource', // Image source (3 == DSC) 120 | 0xa301: 'SceneType', // Scene type (1 == directly photographed) 121 | 0xa302: 'CFAPattern', // Color filter array geometric pattern 122 | 0xa401: 'CustomRendered', // Special processing 123 | 0xa402: 'ExposureMode', // Exposure mode 124 | 0xa403: 'WhiteBalance', // 1 = auto white balance, 2 = manual 125 | 0xa404: 'DigitalZoomRatio', // Digital zoom ratio 126 | 0xa405: 'FocalLengthIn35mmFilm', 127 | 0xa406: 'SceneCaptureType', // Type of scene 128 | 0xa407: 'GainControl', // Degree of overall image gain adjustment 129 | 0xa408: 'Contrast', // Direction of contrast processing applied by camera 130 | 0xa409: 'Saturation', // Direction of saturation processing applied by camera 131 | 0xa40a: 'Sharpness', // Direction of sharpness processing applied by camera 132 | 0xa40b: 'DeviceSettingDescription', 133 | 0xa40c: 'SubjectDistanceRange', // Distance to subject 134 | 0xa420: 'ImageUniqueID', // Identifier assigned uniquely to each image 135 | 0xa430: 'CameraOwnerName', 136 | 0xa431: 'BodySerialNumber', 137 | 0xa432: 'LensSpecification', 138 | 0xa433: 'LensMake', 139 | 0xa434: 'LensModel', 140 | 0xa435: 'LensSerialNumber' 141 | }, 142 | 0x8825: { 143 | // GPSInfoIFDPointer 144 | 0x0000: 'GPSVersionID', 145 | 0x0001: 'GPSLatitudeRef', 146 | 0x0002: 'GPSLatitude', 147 | 0x0003: 'GPSLongitudeRef', 148 | 0x0004: 'GPSLongitude', 149 | 0x0005: 'GPSAltitudeRef', 150 | 0x0006: 'GPSAltitude', 151 | 0x0007: 'GPSTimeStamp', 152 | 0x0008: 'GPSSatellites', 153 | 0x0009: 'GPSStatus', 154 | 0x000a: 'GPSMeasureMode', 155 | 0x000b: 'GPSDOP', 156 | 0x000c: 'GPSSpeedRef', 157 | 0x000d: 'GPSSpeed', 158 | 0x000e: 'GPSTrackRef', 159 | 0x000f: 'GPSTrack', 160 | 0x0010: 'GPSImgDirectionRef', 161 | 0x0011: 'GPSImgDirection', 162 | 0x0012: 'GPSMapDatum', 163 | 0x0013: 'GPSDestLatitudeRef', 164 | 0x0014: 'GPSDestLatitude', 165 | 0x0015: 'GPSDestLongitudeRef', 166 | 0x0016: 'GPSDestLongitude', 167 | 0x0017: 'GPSDestBearingRef', 168 | 0x0018: 'GPSDestBearing', 169 | 0x0019: 'GPSDestDistanceRef', 170 | 0x001a: 'GPSDestDistance', 171 | 0x001b: 'GPSProcessingMethod', 172 | 0x001c: 'GPSAreaInformation', 173 | 0x001d: 'GPSDateStamp', 174 | 0x001e: 'GPSDifferential', 175 | 0x001f: 'GPSHPositioningError' 176 | }, 177 | 0xa005: { 178 | // InteroperabilityIFDPointer 179 | 0x0001: 'InteroperabilityIndex' 180 | } 181 | } 182 | 183 | // IFD1 directory can contain any IFD0 tags: 184 | ExifMapProto.tags.ifd1 = ExifMapProto.tags 185 | 186 | ExifMapProto.stringValues = { 187 | ExposureProgram: { 188 | 0: 'Undefined', 189 | 1: 'Manual', 190 | 2: 'Normal program', 191 | 3: 'Aperture priority', 192 | 4: 'Shutter priority', 193 | 5: 'Creative program', 194 | 6: 'Action program', 195 | 7: 'Portrait mode', 196 | 8: 'Landscape mode' 197 | }, 198 | MeteringMode: { 199 | 0: 'Unknown', 200 | 1: 'Average', 201 | 2: 'CenterWeightedAverage', 202 | 3: 'Spot', 203 | 4: 'MultiSpot', 204 | 5: 'Pattern', 205 | 6: 'Partial', 206 | 255: 'Other' 207 | }, 208 | LightSource: { 209 | 0: 'Unknown', 210 | 1: 'Daylight', 211 | 2: 'Fluorescent', 212 | 3: 'Tungsten (incandescent light)', 213 | 4: 'Flash', 214 | 9: 'Fine weather', 215 | 10: 'Cloudy weather', 216 | 11: 'Shade', 217 | 12: 'Daylight fluorescent (D 5700 - 7100K)', 218 | 13: 'Day white fluorescent (N 4600 - 5400K)', 219 | 14: 'Cool white fluorescent (W 3900 - 4500K)', 220 | 15: 'White fluorescent (WW 3200 - 3700K)', 221 | 17: 'Standard light A', 222 | 18: 'Standard light B', 223 | 19: 'Standard light C', 224 | 20: 'D55', 225 | 21: 'D65', 226 | 22: 'D75', 227 | 23: 'D50', 228 | 24: 'ISO studio tungsten', 229 | 255: 'Other' 230 | }, 231 | Flash: { 232 | 0x0000: 'Flash did not fire', 233 | 0x0001: 'Flash fired', 234 | 0x0005: 'Strobe return light not detected', 235 | 0x0007: 'Strobe return light detected', 236 | 0x0009: 'Flash fired, compulsory flash mode', 237 | 0x000d: 'Flash fired, compulsory flash mode, return light not detected', 238 | 0x000f: 'Flash fired, compulsory flash mode, return light detected', 239 | 0x0010: 'Flash did not fire, compulsory flash mode', 240 | 0x0018: 'Flash did not fire, auto mode', 241 | 0x0019: 'Flash fired, auto mode', 242 | 0x001d: 'Flash fired, auto mode, return light not detected', 243 | 0x001f: 'Flash fired, auto mode, return light detected', 244 | 0x0020: 'No flash function', 245 | 0x0041: 'Flash fired, red-eye reduction mode', 246 | 0x0045: 'Flash fired, red-eye reduction mode, return light not detected', 247 | 0x0047: 'Flash fired, red-eye reduction mode, return light detected', 248 | 0x0049: 'Flash fired, compulsory flash mode, red-eye reduction mode', 249 | 0x004d: 250 | 'Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected', 251 | 0x004f: 252 | 'Flash fired, compulsory flash mode, red-eye reduction mode, return light detected', 253 | 0x0059: 'Flash fired, auto mode, red-eye reduction mode', 254 | 0x005d: 255 | 'Flash fired, auto mode, return light not detected, red-eye reduction mode', 256 | 0x005f: 257 | 'Flash fired, auto mode, return light detected, red-eye reduction mode' 258 | }, 259 | SensingMethod: { 260 | 1: 'Undefined', 261 | 2: 'One-chip color area sensor', 262 | 3: 'Two-chip color area sensor', 263 | 4: 'Three-chip color area sensor', 264 | 5: 'Color sequential area sensor', 265 | 7: 'Trilinear sensor', 266 | 8: 'Color sequential linear sensor' 267 | }, 268 | SceneCaptureType: { 269 | 0: 'Standard', 270 | 1: 'Landscape', 271 | 2: 'Portrait', 272 | 3: 'Night scene' 273 | }, 274 | SceneType: { 275 | 1: 'Directly photographed' 276 | }, 277 | CustomRendered: { 278 | 0: 'Normal process', 279 | 1: 'Custom process' 280 | }, 281 | WhiteBalance: { 282 | 0: 'Auto white balance', 283 | 1: 'Manual white balance' 284 | }, 285 | GainControl: { 286 | 0: 'None', 287 | 1: 'Low gain up', 288 | 2: 'High gain up', 289 | 3: 'Low gain down', 290 | 4: 'High gain down' 291 | }, 292 | Contrast: { 293 | 0: 'Normal', 294 | 1: 'Soft', 295 | 2: 'Hard' 296 | }, 297 | Saturation: { 298 | 0: 'Normal', 299 | 1: 'Low saturation', 300 | 2: 'High saturation' 301 | }, 302 | Sharpness: { 303 | 0: 'Normal', 304 | 1: 'Soft', 305 | 2: 'Hard' 306 | }, 307 | SubjectDistanceRange: { 308 | 0: 'Unknown', 309 | 1: 'Macro', 310 | 2: 'Close view', 311 | 3: 'Distant view' 312 | }, 313 | FileSource: { 314 | 3: 'DSC' 315 | }, 316 | ComponentsConfiguration: { 317 | 0: '', 318 | 1: 'Y', 319 | 2: 'Cb', 320 | 3: 'Cr', 321 | 4: 'R', 322 | 5: 'G', 323 | 6: 'B' 324 | }, 325 | Orientation: { 326 | 1: 'Original', 327 | 2: 'Horizontal flip', 328 | 3: 'Rotate 180° CCW', 329 | 4: 'Vertical flip', 330 | 5: 'Vertical flip + Rotate 90° CW', 331 | 6: 'Rotate 90° CW', 332 | 7: 'Horizontal flip + Rotate 90° CW', 333 | 8: 'Rotate 90° CCW' 334 | } 335 | } 336 | 337 | ExifMapProto.getText = function (name) { 338 | var value = this.get(name) 339 | switch (name) { 340 | case 'LightSource': 341 | case 'Flash': 342 | case 'MeteringMode': 343 | case 'ExposureProgram': 344 | case 'SensingMethod': 345 | case 'SceneCaptureType': 346 | case 'SceneType': 347 | case 'CustomRendered': 348 | case 'WhiteBalance': 349 | case 'GainControl': 350 | case 'Contrast': 351 | case 'Saturation': 352 | case 'Sharpness': 353 | case 'SubjectDistanceRange': 354 | case 'FileSource': 355 | case 'Orientation': 356 | return this.stringValues[name][value] 357 | case 'ExifVersion': 358 | case 'FlashpixVersion': 359 | if (!value) return 360 | return String.fromCharCode(value[0], value[1], value[2], value[3]) 361 | case 'ComponentsConfiguration': 362 | if (!value) return 363 | return ( 364 | this.stringValues[name][value[0]] + 365 | this.stringValues[name][value[1]] + 366 | this.stringValues[name][value[2]] + 367 | this.stringValues[name][value[3]] 368 | ) 369 | case 'GPSVersionID': 370 | if (!value) return 371 | return value[0] + '.' + value[1] + '.' + value[2] + '.' + value[3] 372 | } 373 | return String(value) 374 | } 375 | 376 | ExifMapProto.getAll = function () { 377 | var map = {} 378 | var prop 379 | var obj 380 | var name 381 | for (prop in this) { 382 | if (Object.prototype.hasOwnProperty.call(this, prop)) { 383 | obj = this[prop] 384 | if (obj && obj.getAll) { 385 | map[this.ifds[prop].name] = obj.getAll() 386 | } else { 387 | name = this.tags[prop] 388 | if (name) map[name] = this.getText(name) 389 | } 390 | } 391 | } 392 | return map 393 | } 394 | 395 | ExifMapProto.getName = function (tagCode) { 396 | var name = this.tags[tagCode] 397 | if (typeof name === 'object') return this.ifds[tagCode].name 398 | return name 399 | } 400 | 401 | // Extend the map of tag names to tag codes: 402 | ;(function () { 403 | var tags = ExifMapProto.tags 404 | var prop 405 | var ifd 406 | var subTags 407 | // Map the tag names to tags: 408 | for (prop in tags) { 409 | if (Object.prototype.hasOwnProperty.call(tags, prop)) { 410 | ifd = ExifMapProto.ifds[prop] 411 | if (ifd) { 412 | subTags = tags[prop] 413 | for (prop in subTags) { 414 | if (Object.prototype.hasOwnProperty.call(subTags, prop)) { 415 | ifd.map[subTags[prop]] = Number(prop) 416 | } 417 | } 418 | } else { 419 | ExifMapProto.map[tags[prop]] = Number(prop) 420 | } 421 | } 422 | } 423 | })() 424 | }) 425 | -------------------------------------------------------------------------------- /js/load-image-exif.js: -------------------------------------------------------------------------------- 1 | /* 2 | * JavaScript Load Image Exif Parser 3 | * https://github.com/blueimp/JavaScript-Load-Image 4 | * 5 | * Copyright 2013, Sebastian Tschan 6 | * https://blueimp.net 7 | * 8 | * Licensed under the MIT license: 9 | * https://opensource.org/licenses/MIT 10 | */ 11 | 12 | /* global define, module, require, DataView */ 13 | 14 | /* eslint-disable no-console */ 15 | 16 | ;(function (factory) { 17 | 'use strict' 18 | if (typeof define === 'function' && define.amd) { 19 | // Register as an anonymous AMD module: 20 | define(['./load-image', './load-image-meta'], factory) 21 | } else if (typeof module === 'object' && module.exports) { 22 | factory(require('./load-image'), require('./load-image-meta')) 23 | } else { 24 | // Browser globals: 25 | factory(window.loadImage) 26 | } 27 | })(function (loadImage) { 28 | 'use strict' 29 | 30 | /** 31 | * Exif tag map 32 | * 33 | * @name ExifMap 34 | * @class 35 | * @param {number|string} tagCode IFD tag code 36 | */ 37 | function ExifMap(tagCode) { 38 | if (tagCode) { 39 | Object.defineProperty(this, 'map', { 40 | value: this.ifds[tagCode].map 41 | }) 42 | Object.defineProperty(this, 'tags', { 43 | value: (this.tags && this.tags[tagCode]) || {} 44 | }) 45 | } 46 | } 47 | 48 | ExifMap.prototype.map = { 49 | Orientation: 0x0112, 50 | Thumbnail: 'ifd1', 51 | Blob: 0x0201, // Alias for JPEGInterchangeFormat 52 | Exif: 0x8769, 53 | GPSInfo: 0x8825, 54 | Interoperability: 0xa005 55 | } 56 | 57 | ExifMap.prototype.ifds = { 58 | ifd1: { name: 'Thumbnail', map: ExifMap.prototype.map }, 59 | 0x8769: { name: 'Exif', map: {} }, 60 | 0x8825: { name: 'GPSInfo', map: {} }, 61 | 0xa005: { name: 'Interoperability', map: {} } 62 | } 63 | 64 | /** 65 | * Retrieves exif tag value 66 | * 67 | * @param {number|string} id Exif tag code or name 68 | * @returns {object} Exif tag value 69 | */ 70 | ExifMap.prototype.get = function (id) { 71 | return this[id] || this[this.map[id]] 72 | } 73 | 74 | /** 75 | * Returns the Exif Thumbnail data as Blob. 76 | * 77 | * @param {DataView} dataView Data view interface 78 | * @param {number} offset Thumbnail data offset 79 | * @param {number} length Thumbnail data length 80 | * @returns {undefined|Blob} Returns the Thumbnail Blob or undefined 81 | */ 82 | function getExifThumbnail(dataView, offset, length) { 83 | if (!length) return 84 | if (offset + length > dataView.byteLength) { 85 | console.log('Invalid Exif data: Invalid thumbnail data.') 86 | return 87 | } 88 | return new Blob( 89 | [loadImage.bufferSlice.call(dataView.buffer, offset, offset + length)], 90 | { 91 | type: 'image/jpeg' 92 | } 93 | ) 94 | } 95 | 96 | var ExifTagTypes = { 97 | // byte, 8-bit unsigned int: 98 | 1: { 99 | getValue: function (dataView, dataOffset) { 100 | return dataView.getUint8(dataOffset) 101 | }, 102 | size: 1 103 | }, 104 | // ascii, 8-bit byte: 105 | 2: { 106 | getValue: function (dataView, dataOffset) { 107 | return String.fromCharCode(dataView.getUint8(dataOffset)) 108 | }, 109 | size: 1, 110 | ascii: true 111 | }, 112 | // short, 16 bit int: 113 | 3: { 114 | getValue: function (dataView, dataOffset, littleEndian) { 115 | return dataView.getUint16(dataOffset, littleEndian) 116 | }, 117 | size: 2 118 | }, 119 | // long, 32 bit int: 120 | 4: { 121 | getValue: function (dataView, dataOffset, littleEndian) { 122 | return dataView.getUint32(dataOffset, littleEndian) 123 | }, 124 | size: 4 125 | }, 126 | // rational = two long values, first is numerator, second is denominator: 127 | 5: { 128 | getValue: function (dataView, dataOffset, littleEndian) { 129 | return ( 130 | dataView.getUint32(dataOffset, littleEndian) / 131 | dataView.getUint32(dataOffset + 4, littleEndian) 132 | ) 133 | }, 134 | size: 8 135 | }, 136 | // slong, 32 bit signed int: 137 | 9: { 138 | getValue: function (dataView, dataOffset, littleEndian) { 139 | return dataView.getInt32(dataOffset, littleEndian) 140 | }, 141 | size: 4 142 | }, 143 | // srational, two slongs, first is numerator, second is denominator: 144 | 10: { 145 | getValue: function (dataView, dataOffset, littleEndian) { 146 | return ( 147 | dataView.getInt32(dataOffset, littleEndian) / 148 | dataView.getInt32(dataOffset + 4, littleEndian) 149 | ) 150 | }, 151 | size: 8 152 | } 153 | } 154 | // undefined, 8-bit byte, value depending on field: 155 | ExifTagTypes[7] = ExifTagTypes[1] 156 | 157 | /** 158 | * Returns Exif tag value. 159 | * 160 | * @param {DataView} dataView Data view interface 161 | * @param {number} tiffOffset TIFF offset 162 | * @param {number} offset Tag offset 163 | * @param {number} type Tag type 164 | * @param {number} length Tag length 165 | * @param {boolean} littleEndian Little endian encoding 166 | * @returns {object} Tag value 167 | */ 168 | function getExifValue( 169 | dataView, 170 | tiffOffset, 171 | offset, 172 | type, 173 | length, 174 | littleEndian 175 | ) { 176 | var tagType = ExifTagTypes[type] 177 | var tagSize 178 | var dataOffset 179 | var values 180 | var i 181 | var str 182 | var c 183 | if (!tagType) { 184 | console.log('Invalid Exif data: Invalid tag type.') 185 | return 186 | } 187 | tagSize = tagType.size * length 188 | // Determine if the value is contained in the dataOffset bytes, 189 | // or if the value at the dataOffset is a pointer to the actual data: 190 | dataOffset = 191 | tagSize > 4 192 | ? tiffOffset + dataView.getUint32(offset + 8, littleEndian) 193 | : offset + 8 194 | if (dataOffset + tagSize > dataView.byteLength) { 195 | console.log('Invalid Exif data: Invalid data offset.') 196 | return 197 | } 198 | if (length === 1) { 199 | return tagType.getValue(dataView, dataOffset, littleEndian) 200 | } 201 | values = [] 202 | for (i = 0; i < length; i += 1) { 203 | values[i] = tagType.getValue( 204 | dataView, 205 | dataOffset + i * tagType.size, 206 | littleEndian 207 | ) 208 | } 209 | if (tagType.ascii) { 210 | str = '' 211 | // Concatenate the chars: 212 | for (i = 0; i < values.length; i += 1) { 213 | c = values[i] 214 | // Ignore the terminating NULL byte(s): 215 | if (c === '\u0000') { 216 | break 217 | } 218 | str += c 219 | } 220 | return str 221 | } 222 | return values 223 | } 224 | 225 | /** 226 | * Determines if the given tag should be included. 227 | * 228 | * @param {object} includeTags Map of tags to include 229 | * @param {object} excludeTags Map of tags to exclude 230 | * @param {number|string} tagCode Tag code to check 231 | * @returns {boolean} True if the tag should be included 232 | */ 233 | function shouldIncludeTag(includeTags, excludeTags, tagCode) { 234 | return ( 235 | (!includeTags || includeTags[tagCode]) && 236 | (!excludeTags || excludeTags[tagCode] !== true) 237 | ) 238 | } 239 | 240 | /** 241 | * Parses Exif tags. 242 | * 243 | * @param {DataView} dataView Data view interface 244 | * @param {number} tiffOffset TIFF offset 245 | * @param {number} dirOffset Directory offset 246 | * @param {boolean} littleEndian Little endian encoding 247 | * @param {ExifMap} tags Map to store parsed exif tags 248 | * @param {ExifMap} tagOffsets Map to store parsed exif tag offsets 249 | * @param {object} includeTags Map of tags to include 250 | * @param {object} excludeTags Map of tags to exclude 251 | * @returns {number} Next directory offset 252 | */ 253 | function parseExifTags( 254 | dataView, 255 | tiffOffset, 256 | dirOffset, 257 | littleEndian, 258 | tags, 259 | tagOffsets, 260 | includeTags, 261 | excludeTags 262 | ) { 263 | var tagsNumber, dirEndOffset, i, tagOffset, tagNumber, tagValue 264 | if (dirOffset + 6 > dataView.byteLength) { 265 | console.log('Invalid Exif data: Invalid directory offset.') 266 | return 267 | } 268 | tagsNumber = dataView.getUint16(dirOffset, littleEndian) 269 | dirEndOffset = dirOffset + 2 + 12 * tagsNumber 270 | if (dirEndOffset + 4 > dataView.byteLength) { 271 | console.log('Invalid Exif data: Invalid directory size.') 272 | return 273 | } 274 | for (i = 0; i < tagsNumber; i += 1) { 275 | tagOffset = dirOffset + 2 + 12 * i 276 | tagNumber = dataView.getUint16(tagOffset, littleEndian) 277 | if (!shouldIncludeTag(includeTags, excludeTags, tagNumber)) continue 278 | tagValue = getExifValue( 279 | dataView, 280 | tiffOffset, 281 | tagOffset, 282 | dataView.getUint16(tagOffset + 2, littleEndian), // tag type 283 | dataView.getUint32(tagOffset + 4, littleEndian), // tag length 284 | littleEndian 285 | ) 286 | tags[tagNumber] = tagValue 287 | if (tagOffsets) { 288 | tagOffsets[tagNumber] = tagOffset 289 | } 290 | } 291 | // Return the offset to the next directory: 292 | return dataView.getUint32(dirEndOffset, littleEndian) 293 | } 294 | 295 | /** 296 | * Parses tags in a given IFD (Image File Directory). 297 | * 298 | * @param {object} data Data object to store exif tags and offsets 299 | * @param {number|string} tagCode IFD tag code 300 | * @param {DataView} dataView Data view interface 301 | * @param {number} tiffOffset TIFF offset 302 | * @param {boolean} littleEndian Little endian encoding 303 | * @param {object} includeTags Map of tags to include 304 | * @param {object} excludeTags Map of tags to exclude 305 | */ 306 | function parseExifIFD( 307 | data, 308 | tagCode, 309 | dataView, 310 | tiffOffset, 311 | littleEndian, 312 | includeTags, 313 | excludeTags 314 | ) { 315 | var dirOffset = data.exif[tagCode] 316 | if (dirOffset) { 317 | data.exif[tagCode] = new ExifMap(tagCode) 318 | if (data.exifOffsets) { 319 | data.exifOffsets[tagCode] = new ExifMap(tagCode) 320 | } 321 | parseExifTags( 322 | dataView, 323 | tiffOffset, 324 | tiffOffset + dirOffset, 325 | littleEndian, 326 | data.exif[tagCode], 327 | data.exifOffsets && data.exifOffsets[tagCode], 328 | includeTags && includeTags[tagCode], 329 | excludeTags && excludeTags[tagCode] 330 | ) 331 | } 332 | } 333 | 334 | loadImage.parseExifData = function (dataView, offset, length, data, options) { 335 | if (options.disableExif) { 336 | return 337 | } 338 | var includeTags = options.includeExifTags 339 | var excludeTags = options.excludeExifTags || { 340 | 0x8769: { 341 | // ExifIFDPointer 342 | 0x927c: true // MakerNote 343 | } 344 | } 345 | var tiffOffset = offset + 10 346 | var littleEndian 347 | var dirOffset 348 | var thumbnailIFD 349 | // Check for the ASCII code for "Exif" (0x45786966): 350 | if (dataView.getUint32(offset + 4) !== 0x45786966) { 351 | // No Exif data, might be XMP data instead 352 | return 353 | } 354 | if (tiffOffset + 8 > dataView.byteLength) { 355 | console.log('Invalid Exif data: Invalid segment size.') 356 | return 357 | } 358 | // Check for the two null bytes: 359 | if (dataView.getUint16(offset + 8) !== 0x0000) { 360 | console.log('Invalid Exif data: Missing byte alignment offset.') 361 | return 362 | } 363 | // Check the byte alignment: 364 | switch (dataView.getUint16(tiffOffset)) { 365 | case 0x4949: 366 | littleEndian = true 367 | break 368 | case 0x4d4d: 369 | littleEndian = false 370 | break 371 | default: 372 | console.log('Invalid Exif data: Invalid byte alignment marker.') 373 | return 374 | } 375 | // Check for the TIFF tag marker (0x002A): 376 | if (dataView.getUint16(tiffOffset + 2, littleEndian) !== 0x002a) { 377 | console.log('Invalid Exif data: Missing TIFF marker.') 378 | return 379 | } 380 | // Retrieve the directory offset bytes, usually 0x00000008 or 8 decimal: 381 | dirOffset = dataView.getUint32(tiffOffset + 4, littleEndian) 382 | // Create the exif object to store the tags: 383 | data.exif = new ExifMap() 384 | if (!options.disableExifOffsets) { 385 | data.exifOffsets = new ExifMap() 386 | data.exifTiffOffset = tiffOffset 387 | data.exifLittleEndian = littleEndian 388 | } 389 | // Parse the tags of the main image directory (IFD0) and retrieve the 390 | // offset to the next directory (IFD1), usually the thumbnail directory: 391 | dirOffset = parseExifTags( 392 | dataView, 393 | tiffOffset, 394 | tiffOffset + dirOffset, 395 | littleEndian, 396 | data.exif, 397 | data.exifOffsets, 398 | includeTags, 399 | excludeTags 400 | ) 401 | if (dirOffset && shouldIncludeTag(includeTags, excludeTags, 'ifd1')) { 402 | data.exif.ifd1 = dirOffset 403 | if (data.exifOffsets) { 404 | data.exifOffsets.ifd1 = tiffOffset + dirOffset 405 | } 406 | } 407 | Object.keys(data.exif.ifds).forEach(function (tagCode) { 408 | parseExifIFD( 409 | data, 410 | tagCode, 411 | dataView, 412 | tiffOffset, 413 | littleEndian, 414 | includeTags, 415 | excludeTags 416 | ) 417 | }) 418 | thumbnailIFD = data.exif.ifd1 419 | // Check for JPEG Thumbnail offset and data length: 420 | if (thumbnailIFD && thumbnailIFD[0x0201]) { 421 | thumbnailIFD[0x0201] = getExifThumbnail( 422 | dataView, 423 | tiffOffset + thumbnailIFD[0x0201], 424 | thumbnailIFD[0x0202] // Thumbnail data length 425 | ) 426 | } 427 | } 428 | 429 | // Registers the Exif parser for the APP1 JPEG metadata segment: 430 | loadImage.metaDataParsers.jpeg[0xffe1].push(loadImage.parseExifData) 431 | 432 | loadImage.exifWriters = { 433 | // Orientation writer: 434 | 0x0112: function (buffer, data, value) { 435 | var orientationOffset = data.exifOffsets[0x0112] 436 | if (!orientationOffset) return buffer 437 | var view = new DataView(buffer, orientationOffset + 8, 2) 438 | view.setUint16(0, value, data.exifLittleEndian) 439 | return buffer 440 | } 441 | } 442 | 443 | loadImage.writeExifData = function (buffer, data, id, value) { 444 | return loadImage.exifWriters[data.exif.map[id]](buffer, data, value) 445 | } 446 | 447 | loadImage.ExifMap = ExifMap 448 | 449 | // Adds the following properties to the parseMetaData callback data: 450 | // - exif: The parsed Exif tags 451 | // - exifOffsets: The parsed Exif tag offsets 452 | // - exifTiffOffset: TIFF header offset (used for offset pointers) 453 | // - exifLittleEndian: little endian order if true, big endian if false 454 | 455 | // Adds the following options to the parseMetaData method: 456 | // - disableExif: Disables Exif parsing when true. 457 | // - disableExifOffsets: Disables storing Exif tag offsets when true. 458 | // - includeExifTags: A map of Exif tags to include for parsing. 459 | // - excludeExifTags: A map of Exif tags to exclude from parsing. 460 | }) 461 | -------------------------------------------------------------------------------- /js/load-image-fetch.js: -------------------------------------------------------------------------------- 1 | /* 2 | * JavaScript Load Image Fetch 3 | * https://github.com/blueimp/JavaScript-Load-Image 4 | * 5 | * Copyright 2017, Sebastian Tschan 6 | * https://blueimp.net 7 | * 8 | * Licensed under the MIT license: 9 | * https://opensource.org/licenses/MIT 10 | */ 11 | 12 | /* global define, module, require, Promise */ 13 | 14 | ;(function (factory) { 15 | 'use strict' 16 | if (typeof define === 'function' && define.amd) { 17 | // Register as an anonymous AMD module: 18 | define(['./load-image'], factory) 19 | } else if (typeof module === 'object' && module.exports) { 20 | factory(require('./load-image')) 21 | } else { 22 | // Browser globals: 23 | factory(window.loadImage) 24 | } 25 | })(function (loadImage) { 26 | 'use strict' 27 | 28 | var global = loadImage.global 29 | 30 | if ( 31 | global.fetch && 32 | global.Request && 33 | global.Response && 34 | global.Response.prototype.blob 35 | ) { 36 | loadImage.fetchBlob = function (url, callback, options) { 37 | /** 38 | * Fetch response handler. 39 | * 40 | * @param {Response} response Fetch response 41 | * @returns {Blob} Fetched Blob. 42 | */ 43 | function responseHandler(response) { 44 | return response.blob() 45 | } 46 | if (global.Promise && typeof callback !== 'function') { 47 | return fetch(new Request(url, callback)).then(responseHandler) 48 | } 49 | fetch(new Request(url, options)) 50 | .then(responseHandler) 51 | .then(callback) 52 | [ 53 | // Avoid parsing error in IE<9, where catch is a reserved word. 54 | // eslint-disable-next-line dot-notation 55 | 'catch' 56 | ](function (err) { 57 | callback(null, err) 58 | }) 59 | } 60 | } else if ( 61 | global.XMLHttpRequest && 62 | // https://xhr.spec.whatwg.org/#the-responsetype-attribute 63 | new XMLHttpRequest().responseType === '' 64 | ) { 65 | loadImage.fetchBlob = function (url, callback, options) { 66 | /** 67 | * Promise executor 68 | * 69 | * @param {Function} resolve Resolution function 70 | * @param {Function} reject Rejection function 71 | */ 72 | function executor(resolve, reject) { 73 | options = options || {} // eslint-disable-line no-param-reassign 74 | var req = new XMLHttpRequest() 75 | req.open(options.method || 'GET', url) 76 | if (options.headers) { 77 | Object.keys(options.headers).forEach(function (key) { 78 | req.setRequestHeader(key, options.headers[key]) 79 | }) 80 | } 81 | req.withCredentials = options.credentials === 'include' 82 | req.responseType = 'blob' 83 | req.onload = function () { 84 | resolve(req.response) 85 | } 86 | req.onerror = 87 | req.onabort = 88 | req.ontimeout = 89 | function (err) { 90 | if (resolve === reject) { 91 | // Not using Promises 92 | reject(null, err) 93 | } else { 94 | reject(err) 95 | } 96 | } 97 | req.send(options.body) 98 | } 99 | if (global.Promise && typeof callback !== 'function') { 100 | options = callback // eslint-disable-line no-param-reassign 101 | return new Promise(executor) 102 | } 103 | return executor(callback, callback) 104 | } 105 | } 106 | }) 107 | -------------------------------------------------------------------------------- /js/load-image-iptc-map.js: -------------------------------------------------------------------------------- 1 | /* 2 | * JavaScript Load Image IPTC Map 3 | * https://github.com/blueimp/JavaScript-Load-Image 4 | * 5 | * Copyright 2013, Sebastian Tschan 6 | * Copyright 2018, Dave Bevan 7 | * 8 | * IPTC tags mapping based on 9 | * https://iptc.org/standards/photo-metadata 10 | * https://exiftool.org/TagNames/IPTC.html 11 | * 12 | * Licensed under the MIT license: 13 | * https://opensource.org/licenses/MIT 14 | */ 15 | 16 | /* global define, module, require */ 17 | 18 | ;(function (factory) { 19 | 'use strict' 20 | if (typeof define === 'function' && define.amd) { 21 | // Register as an anonymous AMD module: 22 | define(['./load-image', './load-image-iptc'], factory) 23 | } else if (typeof module === 'object' && module.exports) { 24 | factory(require('./load-image'), require('./load-image-iptc')) 25 | } else { 26 | // Browser globals: 27 | factory(window.loadImage) 28 | } 29 | })(function (loadImage) { 30 | 'use strict' 31 | 32 | var IptcMapProto = loadImage.IptcMap.prototype 33 | 34 | IptcMapProto.tags = { 35 | 0: 'ApplicationRecordVersion', 36 | 3: 'ObjectTypeReference', 37 | 4: 'ObjectAttributeReference', 38 | 5: 'ObjectName', 39 | 7: 'EditStatus', 40 | 8: 'EditorialUpdate', 41 | 10: 'Urgency', 42 | 12: 'SubjectReference', 43 | 15: 'Category', 44 | 20: 'SupplementalCategories', 45 | 22: 'FixtureIdentifier', 46 | 25: 'Keywords', 47 | 26: 'ContentLocationCode', 48 | 27: 'ContentLocationName', 49 | 30: 'ReleaseDate', 50 | 35: 'ReleaseTime', 51 | 37: 'ExpirationDate', 52 | 38: 'ExpirationTime', 53 | 40: 'SpecialInstructions', 54 | 42: 'ActionAdvised', 55 | 45: 'ReferenceService', 56 | 47: 'ReferenceDate', 57 | 50: 'ReferenceNumber', 58 | 55: 'DateCreated', 59 | 60: 'TimeCreated', 60 | 62: 'DigitalCreationDate', 61 | 63: 'DigitalCreationTime', 62 | 65: 'OriginatingProgram', 63 | 70: 'ProgramVersion', 64 | 75: 'ObjectCycle', 65 | 80: 'Byline', 66 | 85: 'BylineTitle', 67 | 90: 'City', 68 | 92: 'Sublocation', 69 | 95: 'State', 70 | 100: 'CountryCode', 71 | 101: 'Country', 72 | 103: 'OriginalTransmissionReference', 73 | 105: 'Headline', 74 | 110: 'Credit', 75 | 115: 'Source', 76 | 116: 'CopyrightNotice', 77 | 118: 'Contact', 78 | 120: 'Caption', 79 | 121: 'LocalCaption', 80 | 122: 'Writer', 81 | 125: 'RasterizedCaption', 82 | 130: 'ImageType', 83 | 131: 'ImageOrientation', 84 | 135: 'LanguageIdentifier', 85 | 150: 'AudioType', 86 | 151: 'AudioSamplingRate', 87 | 152: 'AudioSamplingResolution', 88 | 153: 'AudioDuration', 89 | 154: 'AudioOutcue', 90 | 184: 'JobID', 91 | 185: 'MasterDocumentID', 92 | 186: 'ShortDocumentID', 93 | 187: 'UniqueDocumentID', 94 | 188: 'OwnerID', 95 | 200: 'ObjectPreviewFileFormat', 96 | 201: 'ObjectPreviewFileVersion', 97 | 202: 'ObjectPreviewData', 98 | 221: 'Prefs', 99 | 225: 'ClassifyState', 100 | 228: 'SimilarityIndex', 101 | 230: 'DocumentNotes', 102 | 231: 'DocumentHistory', 103 | 232: 'ExifCameraInfo', 104 | 255: 'CatalogSets' 105 | } 106 | 107 | IptcMapProto.stringValues = { 108 | 10: { 109 | 0: '0 (reserved)', 110 | 1: '1 (most urgent)', 111 | 2: '2', 112 | 3: '3', 113 | 4: '4', 114 | 5: '5 (normal urgency)', 115 | 6: '6', 116 | 7: '7', 117 | 8: '8 (least urgent)', 118 | 9: '9 (user-defined priority)' 119 | }, 120 | 75: { 121 | a: 'Morning', 122 | b: 'Both Morning and Evening', 123 | p: 'Evening' 124 | }, 125 | 131: { 126 | L: 'Landscape', 127 | P: 'Portrait', 128 | S: 'Square' 129 | } 130 | } 131 | 132 | IptcMapProto.getText = function (id) { 133 | var value = this.get(id) 134 | var tagCode = this.map[id] 135 | var stringValue = this.stringValues[tagCode] 136 | if (stringValue) return stringValue[value] 137 | return String(value) 138 | } 139 | 140 | IptcMapProto.getAll = function () { 141 | var map = {} 142 | var prop 143 | var name 144 | for (prop in this) { 145 | if (Object.prototype.hasOwnProperty.call(this, prop)) { 146 | name = this.tags[prop] 147 | if (name) map[name] = this.getText(name) 148 | } 149 | } 150 | return map 151 | } 152 | 153 | IptcMapProto.getName = function (tagCode) { 154 | return this.tags[tagCode] 155 | } 156 | 157 | // Extend the map of tag names to tag codes: 158 | ;(function () { 159 | var tags = IptcMapProto.tags 160 | var map = IptcMapProto.map || {} 161 | var prop 162 | // Map the tag names to tags: 163 | for (prop in tags) { 164 | if (Object.prototype.hasOwnProperty.call(tags, prop)) { 165 | map[tags[prop]] = Number(prop) 166 | } 167 | } 168 | })() 169 | }) 170 | -------------------------------------------------------------------------------- /js/load-image-iptc.js: -------------------------------------------------------------------------------- 1 | /* 2 | * JavaScript Load Image IPTC Parser 3 | * https://github.com/blueimp/JavaScript-Load-Image 4 | * 5 | * Copyright 2013, Sebastian Tschan 6 | * Copyright 2018, Dave Bevan 7 | * https://blueimp.net 8 | * 9 | * Licensed under the MIT license: 10 | * https://opensource.org/licenses/MIT 11 | */ 12 | 13 | /* global define, module, require, DataView */ 14 | 15 | ;(function (factory) { 16 | 'use strict' 17 | if (typeof define === 'function' && define.amd) { 18 | // Register as an anonymous AMD module: 19 | define(['./load-image', './load-image-meta'], factory) 20 | } else if (typeof module === 'object' && module.exports) { 21 | factory(require('./load-image'), require('./load-image-meta')) 22 | } else { 23 | // Browser globals: 24 | factory(window.loadImage) 25 | } 26 | })(function (loadImage) { 27 | 'use strict' 28 | 29 | /** 30 | * IPTC tag map 31 | * 32 | * @name IptcMap 33 | * @class 34 | */ 35 | function IptcMap() {} 36 | 37 | IptcMap.prototype.map = { 38 | ObjectName: 5 39 | } 40 | 41 | IptcMap.prototype.types = { 42 | 0: 'Uint16', // ApplicationRecordVersion 43 | 200: 'Uint16', // ObjectPreviewFileFormat 44 | 201: 'Uint16', // ObjectPreviewFileVersion 45 | 202: 'binary' // ObjectPreviewData 46 | } 47 | 48 | /** 49 | * Retrieves IPTC tag value 50 | * 51 | * @param {number|string} id IPTC tag code or name 52 | * @returns {object} IPTC tag value 53 | */ 54 | IptcMap.prototype.get = function (id) { 55 | return this[id] || this[this.map[id]] 56 | } 57 | 58 | /** 59 | * Retrieves string for the given DataView and range 60 | * 61 | * @param {DataView} dataView Data view interface 62 | * @param {number} offset Offset start 63 | * @param {number} length Offset length 64 | * @returns {string} String value 65 | */ 66 | function getStringValue(dataView, offset, length) { 67 | var outstr = '' 68 | var end = offset + length 69 | for (var n = offset; n < end; n += 1) { 70 | outstr += String.fromCharCode(dataView.getUint8(n)) 71 | } 72 | return outstr 73 | } 74 | 75 | /** 76 | * Retrieves tag value for the given DataView and range 77 | * 78 | * @param {number} tagCode tag code 79 | * @param {IptcMap} map IPTC tag map 80 | * @param {DataView} dataView Data view interface 81 | * @param {number} offset Range start 82 | * @param {number} length Range length 83 | * @returns {object} Tag value 84 | */ 85 | function getTagValue(tagCode, map, dataView, offset, length) { 86 | if (map.types[tagCode] === 'binary') { 87 | return new Blob([dataView.buffer.slice(offset, offset + length)]) 88 | } 89 | if (map.types[tagCode] === 'Uint16') { 90 | return dataView.getUint16(offset) 91 | } 92 | return getStringValue(dataView, offset, length) 93 | } 94 | 95 | /** 96 | * Combines IPTC value with existing ones. 97 | * 98 | * @param {object} value Existing IPTC field value 99 | * @param {object} newValue New IPTC field value 100 | * @returns {object} Resulting IPTC field value 101 | */ 102 | function combineTagValues(value, newValue) { 103 | if (value === undefined) return newValue 104 | if (value instanceof Array) { 105 | value.push(newValue) 106 | return value 107 | } 108 | return [value, newValue] 109 | } 110 | 111 | /** 112 | * Parses IPTC tags. 113 | * 114 | * @param {DataView} dataView Data view interface 115 | * @param {number} segmentOffset Segment offset 116 | * @param {number} segmentLength Segment length 117 | * @param {object} data Data export object 118 | * @param {object} includeTags Map of tags to include 119 | * @param {object} excludeTags Map of tags to exclude 120 | */ 121 | function parseIptcTags( 122 | dataView, 123 | segmentOffset, 124 | segmentLength, 125 | data, 126 | includeTags, 127 | excludeTags 128 | ) { 129 | var value, tagSize, tagCode 130 | var segmentEnd = segmentOffset + segmentLength 131 | var offset = segmentOffset 132 | while (offset < segmentEnd) { 133 | if ( 134 | dataView.getUint8(offset) === 0x1c && // tag marker 135 | dataView.getUint8(offset + 1) === 0x02 // record number, only handles v2 136 | ) { 137 | tagCode = dataView.getUint8(offset + 2) 138 | if ( 139 | (!includeTags || includeTags[tagCode]) && 140 | (!excludeTags || !excludeTags[tagCode]) 141 | ) { 142 | tagSize = dataView.getInt16(offset + 3) 143 | value = getTagValue(tagCode, data.iptc, dataView, offset + 5, tagSize) 144 | data.iptc[tagCode] = combineTagValues(data.iptc[tagCode], value) 145 | if (data.iptcOffsets) { 146 | data.iptcOffsets[tagCode] = offset 147 | } 148 | } 149 | } 150 | offset += 1 151 | } 152 | } 153 | 154 | /** 155 | * Tests if field segment starts at offset. 156 | * 157 | * @param {DataView} dataView Data view interface 158 | * @param {number} offset Segment offset 159 | * @returns {boolean} True if '8BIM' exists at offset 160 | */ 161 | function isSegmentStart(dataView, offset) { 162 | return ( 163 | dataView.getUint32(offset) === 0x3842494d && // Photoshop segment start 164 | dataView.getUint16(offset + 4) === 0x0404 // IPTC segment start 165 | ) 166 | } 167 | 168 | /** 169 | * Returns header length. 170 | * 171 | * @param {DataView} dataView Data view interface 172 | * @param {number} offset Segment offset 173 | * @returns {number} Header length 174 | */ 175 | function getHeaderLength(dataView, offset) { 176 | var length = dataView.getUint8(offset + 7) 177 | if (length % 2 !== 0) length += 1 178 | // Check for pre photoshop 6 format 179 | if (length === 0) { 180 | // Always 4 181 | length = 4 182 | } 183 | return length 184 | } 185 | 186 | loadImage.parseIptcData = function (dataView, offset, length, data, options) { 187 | if (options.disableIptc) { 188 | return 189 | } 190 | var markerLength = offset + length 191 | while (offset + 8 < markerLength) { 192 | if (isSegmentStart(dataView, offset)) { 193 | var headerLength = getHeaderLength(dataView, offset) 194 | var segmentOffset = offset + 8 + headerLength 195 | if (segmentOffset > markerLength) { 196 | // eslint-disable-next-line no-console 197 | console.log('Invalid IPTC data: Invalid segment offset.') 198 | break 199 | } 200 | var segmentLength = dataView.getUint16(offset + 6 + headerLength) 201 | if (offset + segmentLength > markerLength) { 202 | // eslint-disable-next-line no-console 203 | console.log('Invalid IPTC data: Invalid segment size.') 204 | break 205 | } 206 | // Create the iptc object to store the tags: 207 | data.iptc = new IptcMap() 208 | if (!options.disableIptcOffsets) { 209 | data.iptcOffsets = new IptcMap() 210 | } 211 | parseIptcTags( 212 | dataView, 213 | segmentOffset, 214 | segmentLength, 215 | data, 216 | options.includeIptcTags, 217 | options.excludeIptcTags || { 202: true } // ObjectPreviewData 218 | ) 219 | return 220 | } 221 | // eslint-disable-next-line no-param-reassign 222 | offset += 1 223 | } 224 | } 225 | 226 | // Registers this IPTC parser for the APP13 JPEG metadata segment: 227 | loadImage.metaDataParsers.jpeg[0xffed].push(loadImage.parseIptcData) 228 | 229 | loadImage.IptcMap = IptcMap 230 | 231 | // Adds the following properties to the parseMetaData callback data: 232 | // - iptc: The iptc tags, parsed by the parseIptcData method 233 | 234 | // Adds the following options to the parseMetaData method: 235 | // - disableIptc: Disables IPTC parsing when true. 236 | // - disableIptcOffsets: Disables storing IPTC tag offsets when true. 237 | // - includeIptcTags: A map of IPTC tags to include for parsing. 238 | // - excludeIptcTags: A map of IPTC tags to exclude from parsing. 239 | }) 240 | -------------------------------------------------------------------------------- /js/load-image-meta.js: -------------------------------------------------------------------------------- 1 | /* 2 | * JavaScript Load Image Meta 3 | * https://github.com/blueimp/JavaScript-Load-Image 4 | * 5 | * Copyright 2013, Sebastian Tschan 6 | * https://blueimp.net 7 | * 8 | * Image metadata handling implementation 9 | * based on the help and contribution of 10 | * Achim Stöhr. 11 | * 12 | * Licensed under the MIT license: 13 | * https://opensource.org/licenses/MIT 14 | */ 15 | 16 | /* global define, module, require, Promise, DataView, Uint8Array, ArrayBuffer */ 17 | 18 | ;(function (factory) { 19 | 'use strict' 20 | if (typeof define === 'function' && define.amd) { 21 | // Register as an anonymous AMD module: 22 | define(['./load-image'], factory) 23 | } else if (typeof module === 'object' && module.exports) { 24 | factory(require('./load-image')) 25 | } else { 26 | // Browser globals: 27 | factory(window.loadImage) 28 | } 29 | })(function (loadImage) { 30 | 'use strict' 31 | 32 | var global = loadImage.global 33 | var originalTransform = loadImage.transform 34 | 35 | var blobSlice = 36 | global.Blob && 37 | (Blob.prototype.slice || 38 | Blob.prototype.webkitSlice || 39 | Blob.prototype.mozSlice) 40 | 41 | var bufferSlice = 42 | (global.ArrayBuffer && ArrayBuffer.prototype.slice) || 43 | function (begin, end) { 44 | // Polyfill for IE10, which does not support ArrayBuffer.slice 45 | // eslint-disable-next-line no-param-reassign 46 | end = end || this.byteLength - begin 47 | var arr1 = new Uint8Array(this, begin, end) 48 | var arr2 = new Uint8Array(end) 49 | arr2.set(arr1) 50 | return arr2.buffer 51 | } 52 | 53 | var metaDataParsers = { 54 | jpeg: { 55 | 0xffe1: [], // APP1 marker 56 | 0xffed: [] // APP13 marker 57 | } 58 | } 59 | 60 | /** 61 | * Parses image metadata and calls the callback with an object argument 62 | * with the following property: 63 | * - imageHead: The complete image head as ArrayBuffer 64 | * The options argument accepts an object and supports the following 65 | * properties: 66 | * - maxMetaDataSize: Defines the maximum number of bytes to parse. 67 | * - disableImageHead: Disables creating the imageHead property. 68 | * 69 | * @param {Blob} file Blob object 70 | * @param {Function} [callback] Callback function 71 | * @param {object} [options] Parsing options 72 | * @param {object} [data] Result data object 73 | * @returns {Promise|undefined} Returns Promise if no callback given. 74 | */ 75 | function parseMetaData(file, callback, options, data) { 76 | var that = this 77 | /** 78 | * Promise executor 79 | * 80 | * @param {Function} resolve Resolution function 81 | * @param {Function} reject Rejection function 82 | * @returns {undefined} Undefined 83 | */ 84 | function executor(resolve, reject) { 85 | if ( 86 | !( 87 | global.DataView && 88 | blobSlice && 89 | file && 90 | file.size >= 12 && 91 | file.type === 'image/jpeg' 92 | ) 93 | ) { 94 | // Nothing to parse 95 | return resolve(data) 96 | } 97 | // 256 KiB should contain all EXIF/ICC/IPTC segments: 98 | var maxMetaDataSize = options.maxMetaDataSize || 262144 99 | if ( 100 | !loadImage.readFile( 101 | blobSlice.call(file, 0, maxMetaDataSize), 102 | function (buffer) { 103 | // Note on endianness: 104 | // Since the marker and length bytes in JPEG files are always 105 | // stored in big endian order, we can leave the endian parameter 106 | // of the DataView methods undefined, defaulting to big endian. 107 | var dataView = new DataView(buffer) 108 | // Check for the JPEG marker (0xffd8): 109 | if (dataView.getUint16(0) !== 0xffd8) { 110 | return reject( 111 | new Error('Invalid JPEG file: Missing JPEG marker.') 112 | ) 113 | } 114 | var offset = 2 115 | var maxOffset = dataView.byteLength - 4 116 | var headLength = offset 117 | var markerBytes 118 | var markerLength 119 | var parsers 120 | var i 121 | while (offset < maxOffset) { 122 | markerBytes = dataView.getUint16(offset) 123 | // Search for APPn (0xffeN) and COM (0xfffe) markers, 124 | // which contain application-specific metadata like 125 | // Exif, ICC and IPTC data and text comments: 126 | if ( 127 | (markerBytes >= 0xffe0 && markerBytes <= 0xffef) || 128 | markerBytes === 0xfffe 129 | ) { 130 | // The marker bytes (2) are always followed by 131 | // the length bytes (2), indicating the length of the 132 | // marker segment, which includes the length bytes, 133 | // but not the marker bytes, so we add 2: 134 | markerLength = dataView.getUint16(offset + 2) + 2 135 | if (offset + markerLength > dataView.byteLength) { 136 | // eslint-disable-next-line no-console 137 | console.log('Invalid JPEG metadata: Invalid segment size.') 138 | break 139 | } 140 | parsers = metaDataParsers.jpeg[markerBytes] 141 | if (parsers && !options.disableMetaDataParsers) { 142 | for (i = 0; i < parsers.length; i += 1) { 143 | parsers[i].call( 144 | that, 145 | dataView, 146 | offset, 147 | markerLength, 148 | data, 149 | options 150 | ) 151 | } 152 | } 153 | offset += markerLength 154 | headLength = offset 155 | } else { 156 | // Not an APPn or COM marker, probably safe to 157 | // assume that this is the end of the metadata 158 | break 159 | } 160 | } 161 | // Meta length must be longer than JPEG marker (2) 162 | // plus APPn marker (2), followed by length bytes (2): 163 | if (!options.disableImageHead && headLength > 6) { 164 | data.imageHead = bufferSlice.call(buffer, 0, headLength) 165 | } 166 | resolve(data) 167 | }, 168 | reject, 169 | 'readAsArrayBuffer' 170 | ) 171 | ) { 172 | // No support for the FileReader interface, nothing to parse 173 | resolve(data) 174 | } 175 | } 176 | options = options || {} // eslint-disable-line no-param-reassign 177 | if (global.Promise && typeof callback !== 'function') { 178 | options = callback || {} // eslint-disable-line no-param-reassign 179 | data = options // eslint-disable-line no-param-reassign 180 | return new Promise(executor) 181 | } 182 | data = data || {} // eslint-disable-line no-param-reassign 183 | return executor(callback, callback) 184 | } 185 | 186 | /** 187 | * Replaces the head of a JPEG Blob 188 | * 189 | * @param {Blob} blob Blob object 190 | * @param {ArrayBuffer} oldHead Old JPEG head 191 | * @param {ArrayBuffer} newHead New JPEG head 192 | * @returns {Blob} Combined Blob 193 | */ 194 | function replaceJPEGHead(blob, oldHead, newHead) { 195 | if (!blob || !oldHead || !newHead) return null 196 | return new Blob([newHead, blobSlice.call(blob, oldHead.byteLength)], { 197 | type: 'image/jpeg' 198 | }) 199 | } 200 | 201 | /** 202 | * Replaces the image head of a JPEG blob with the given one. 203 | * Returns a Promise or calls the callback with the new Blob. 204 | * 205 | * @param {Blob} blob Blob object 206 | * @param {ArrayBuffer} head New JPEG head 207 | * @param {Function} [callback] Callback function 208 | * @returns {Promise|undefined} Combined Blob 209 | */ 210 | function replaceHead(blob, head, callback) { 211 | var options = { maxMetaDataSize: 1024, disableMetaDataParsers: true } 212 | if (!callback && global.Promise) { 213 | return parseMetaData(blob, options).then(function (data) { 214 | return replaceJPEGHead(blob, data.imageHead, head) 215 | }) 216 | } 217 | parseMetaData( 218 | blob, 219 | function (data) { 220 | callback(replaceJPEGHead(blob, data.imageHead, head)) 221 | }, 222 | options 223 | ) 224 | } 225 | 226 | loadImage.transform = function (img, options, callback, file, data) { 227 | if (loadImage.requiresMetaData(options)) { 228 | data = data || {} // eslint-disable-line no-param-reassign 229 | parseMetaData( 230 | file, 231 | function (result) { 232 | if (result !== data) { 233 | // eslint-disable-next-line no-console 234 | if (global.console) console.log(result) 235 | result = data // eslint-disable-line no-param-reassign 236 | } 237 | originalTransform.call( 238 | loadImage, 239 | img, 240 | options, 241 | callback, 242 | file, 243 | result 244 | ) 245 | }, 246 | options, 247 | data 248 | ) 249 | } else { 250 | originalTransform.apply(loadImage, arguments) 251 | } 252 | } 253 | 254 | loadImage.blobSlice = blobSlice 255 | loadImage.bufferSlice = bufferSlice 256 | loadImage.replaceHead = replaceHead 257 | loadImage.parseMetaData = parseMetaData 258 | loadImage.metaDataParsers = metaDataParsers 259 | }) 260 | -------------------------------------------------------------------------------- /js/load-image-orientation.js: -------------------------------------------------------------------------------- 1 | /* 2 | * JavaScript Load Image Orientation 3 | * https://github.com/blueimp/JavaScript-Load-Image 4 | * 5 | * Copyright 2013, Sebastian Tschan 6 | * https://blueimp.net 7 | * 8 | * Licensed under the MIT license: 9 | * https://opensource.org/licenses/MIT 10 | */ 11 | 12 | /* 13 | Exif orientation values to correctly display the letter F: 14 | 15 | 1 2 16 | ██████ ██████ 17 | ██ ██ 18 | ████ ████ 19 | ██ ██ 20 | ██ ██ 21 | 22 | 3 4 23 | ██ ██ 24 | ██ ██ 25 | ████ ████ 26 | ██ ██ 27 | ██████ ██████ 28 | 29 | 5 6 30 | ██████████ ██ 31 | ██ ██ ██ ██ 32 | ██ ██████████ 33 | 34 | 7 8 35 | ██ ██████████ 36 | ██ ██ ██ ██ 37 | ██████████ ██ 38 | 39 | */ 40 | 41 | /* global define, module, require */ 42 | 43 | ;(function (factory) { 44 | 'use strict' 45 | if (typeof define === 'function' && define.amd) { 46 | // Register as an anonymous AMD module: 47 | define(['./load-image', './load-image-scale', './load-image-meta'], factory) 48 | } else if (typeof module === 'object' && module.exports) { 49 | factory( 50 | require('./load-image'), 51 | require('./load-image-scale'), 52 | require('./load-image-meta') 53 | ) 54 | } else { 55 | // Browser globals: 56 | factory(window.loadImage) 57 | } 58 | })(function (loadImage) { 59 | 'use strict' 60 | 61 | var originalTransform = loadImage.transform 62 | var originalRequiresCanvas = loadImage.requiresCanvas 63 | var originalRequiresMetaData = loadImage.requiresMetaData 64 | var originalTransformCoordinates = loadImage.transformCoordinates 65 | var originalGetTransformedOptions = loadImage.getTransformedOptions 66 | 67 | ;(function ($) { 68 | // Guard for non-browser environments (e.g. server-side rendering): 69 | if (!$.global.document) return 70 | // black+white 3x2 JPEG, with the following meta information set: 71 | // - EXIF Orientation: 6 (Rotated 90° CCW) 72 | // Image data layout (B=black, F=white): 73 | // BFF 74 | // BBB 75 | var testImageURL = 76 | 'data:image/jpeg;base64,/9j/4QAiRXhpZgAATU0AKgAAAAgAAQESAAMAAAABAAYAAAA' + 77 | 'AAAD/2wCEAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA' + 78 | 'QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE' + 79 | 'BAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAf/AABEIAAIAAwMBEQACEQEDEQH/x' + 80 | 'ABRAAEAAAAAAAAAAAAAAAAAAAAKEAEBAQADAQEAAAAAAAAAAAAGBQQDCAkCBwEBAAAAAAA' + 81 | 'AAAAAAAAAAAAAABEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8AG8T9NfSMEVMhQ' + 82 | 'voP3fFiRZ+MTHDifa/95OFSZU5OzRzxkyejv8ciEfhSceSXGjS8eSdLnZc2HDm4M3BxcXw' + 83 | 'H/9k=' 84 | var img = document.createElement('img') 85 | img.onload = function () { 86 | // Check if the browser supports automatic image orientation: 87 | $.orientation = img.width === 2 && img.height === 3 88 | if ($.orientation) { 89 | var canvas = $.createCanvas(1, 1, true) 90 | var ctx = canvas.getContext('2d') 91 | ctx.drawImage(img, 1, 1, 1, 1, 0, 0, 1, 1) 92 | // Check if the source image coordinates (sX, sY, sWidth, sHeight) are 93 | // correctly applied to the auto-orientated image, which should result 94 | // in a white opaque pixel (e.g. in Safari). 95 | // Browsers that show a transparent pixel (e.g. Chromium) fail to crop 96 | // auto-oriented images correctly and require a workaround, e.g. 97 | // drawing the complete source image to an intermediate canvas first. 98 | // See https://bugs.chromium.org/p/chromium/issues/detail?id=1074354 99 | $.orientationCropBug = 100 | ctx.getImageData(0, 0, 1, 1).data.toString() !== '255,255,255,255' 101 | } 102 | } 103 | img.src = testImageURL 104 | })(loadImage) 105 | 106 | /** 107 | * Determines if the orientation requires a canvas element. 108 | * 109 | * @param {object} [options] Options object 110 | * @param {boolean} [withMetaData] Is metadata required for orientation 111 | * @returns {boolean} Returns true if orientation requires canvas/meta 112 | */ 113 | function requiresCanvasOrientation(options, withMetaData) { 114 | var orientation = options && options.orientation 115 | return ( 116 | // Exif orientation for browsers without automatic image orientation: 117 | (orientation === true && !loadImage.orientation) || 118 | // Orientation reset for browsers with automatic image orientation: 119 | (orientation === 1 && loadImage.orientation) || 120 | // Orientation to defined value, requires meta for orientation reset only: 121 | ((!withMetaData || loadImage.orientation) && 122 | orientation > 1 && 123 | orientation < 9) 124 | ) 125 | } 126 | 127 | /** 128 | * Determines if the image requires an orientation change. 129 | * 130 | * @param {number} [orientation] Defined orientation value 131 | * @param {number} [autoOrientation] Auto-orientation based on Exif data 132 | * @returns {boolean} Returns true if an orientation change is required 133 | */ 134 | function requiresOrientationChange(orientation, autoOrientation) { 135 | return ( 136 | orientation !== autoOrientation && 137 | ((orientation === 1 && autoOrientation > 1 && autoOrientation < 9) || 138 | (orientation > 1 && orientation < 9)) 139 | ) 140 | } 141 | 142 | /** 143 | * Determines orientation combinations that require a rotation by 180°. 144 | * 145 | * The following is a list of combinations that return true: 146 | * 147 | * 2 (flip) => 5 (rot90,flip), 7 (rot90,flip), 6 (rot90), 8 (rot90) 148 | * 4 (flip) => 5 (rot90,flip), 7 (rot90,flip), 6 (rot90), 8 (rot90) 149 | * 150 | * 5 (rot90,flip) => 2 (flip), 4 (flip), 6 (rot90), 8 (rot90) 151 | * 7 (rot90,flip) => 2 (flip), 4 (flip), 6 (rot90), 8 (rot90) 152 | * 153 | * 6 (rot90) => 2 (flip), 4 (flip), 5 (rot90,flip), 7 (rot90,flip) 154 | * 8 (rot90) => 2 (flip), 4 (flip), 5 (rot90,flip), 7 (rot90,flip) 155 | * 156 | * @param {number} [orientation] Defined orientation value 157 | * @param {number} [autoOrientation] Auto-orientation based on Exif data 158 | * @returns {boolean} Returns true if rotation by 180° is required 159 | */ 160 | function requiresRot180(orientation, autoOrientation) { 161 | if (autoOrientation > 1 && autoOrientation < 9) { 162 | switch (orientation) { 163 | case 2: 164 | case 4: 165 | return autoOrientation > 4 166 | case 5: 167 | case 7: 168 | return autoOrientation % 2 === 0 169 | case 6: 170 | case 8: 171 | return ( 172 | autoOrientation === 2 || 173 | autoOrientation === 4 || 174 | autoOrientation === 5 || 175 | autoOrientation === 7 176 | ) 177 | } 178 | } 179 | return false 180 | } 181 | 182 | // Determines if the target image should be a canvas element: 183 | loadImage.requiresCanvas = function (options) { 184 | return ( 185 | requiresCanvasOrientation(options) || 186 | originalRequiresCanvas.call(loadImage, options) 187 | ) 188 | } 189 | 190 | // Determines if metadata should be loaded automatically: 191 | loadImage.requiresMetaData = function (options) { 192 | return ( 193 | requiresCanvasOrientation(options, true) || 194 | originalRequiresMetaData.call(loadImage, options) 195 | ) 196 | } 197 | 198 | loadImage.transform = function (img, options, callback, file, data) { 199 | originalTransform.call( 200 | loadImage, 201 | img, 202 | options, 203 | function (img, data) { 204 | if (data) { 205 | var autoOrientation = 206 | loadImage.orientation && data.exif && data.exif.get('Orientation') 207 | if (autoOrientation > 4 && autoOrientation < 9) { 208 | // Automatic image orientation switched image dimensions 209 | var originalWidth = data.originalWidth 210 | var originalHeight = data.originalHeight 211 | data.originalWidth = originalHeight 212 | data.originalHeight = originalWidth 213 | } 214 | } 215 | callback(img, data) 216 | }, 217 | file, 218 | data 219 | ) 220 | } 221 | 222 | // Transforms coordinate and dimension options 223 | // based on the given orientation option: 224 | loadImage.getTransformedOptions = function (img, opts, data) { 225 | var options = originalGetTransformedOptions.call(loadImage, img, opts) 226 | var exifOrientation = data.exif && data.exif.get('Orientation') 227 | var orientation = options.orientation 228 | var autoOrientation = loadImage.orientation && exifOrientation 229 | if (orientation === true) orientation = exifOrientation 230 | if (!requiresOrientationChange(orientation, autoOrientation)) { 231 | return options 232 | } 233 | var top = options.top 234 | var right = options.right 235 | var bottom = options.bottom 236 | var left = options.left 237 | var newOptions = {} 238 | for (var i in options) { 239 | if (Object.prototype.hasOwnProperty.call(options, i)) { 240 | newOptions[i] = options[i] 241 | } 242 | } 243 | newOptions.orientation = orientation 244 | if ( 245 | (orientation > 4 && !(autoOrientation > 4)) || 246 | (orientation < 5 && autoOrientation > 4) 247 | ) { 248 | // Image dimensions and target dimensions are switched 249 | newOptions.maxWidth = options.maxHeight 250 | newOptions.maxHeight = options.maxWidth 251 | newOptions.minWidth = options.minHeight 252 | newOptions.minHeight = options.minWidth 253 | newOptions.sourceWidth = options.sourceHeight 254 | newOptions.sourceHeight = options.sourceWidth 255 | } 256 | if (autoOrientation > 1) { 257 | // Browsers which correctly apply source image coordinates to 258 | // auto-oriented images 259 | switch (autoOrientation) { 260 | case 2: 261 | // Horizontal flip 262 | right = options.left 263 | left = options.right 264 | break 265 | case 3: 266 | // 180° Rotate CCW 267 | top = options.bottom 268 | right = options.left 269 | bottom = options.top 270 | left = options.right 271 | break 272 | case 4: 273 | // Vertical flip 274 | top = options.bottom 275 | bottom = options.top 276 | break 277 | case 5: 278 | // Horizontal flip + 90° Rotate CCW 279 | top = options.left 280 | right = options.bottom 281 | bottom = options.right 282 | left = options.top 283 | break 284 | case 6: 285 | // 90° Rotate CCW 286 | top = options.left 287 | right = options.top 288 | bottom = options.right 289 | left = options.bottom 290 | break 291 | case 7: 292 | // Vertical flip + 90° Rotate CCW 293 | top = options.right 294 | right = options.top 295 | bottom = options.left 296 | left = options.bottom 297 | break 298 | case 8: 299 | // 90° Rotate CW 300 | top = options.right 301 | right = options.bottom 302 | bottom = options.left 303 | left = options.top 304 | break 305 | } 306 | // Some orientation combinations require additional rotation by 180°: 307 | if (requiresRot180(orientation, autoOrientation)) { 308 | var tmpTop = top 309 | var tmpRight = right 310 | top = bottom 311 | right = left 312 | bottom = tmpTop 313 | left = tmpRight 314 | } 315 | } 316 | newOptions.top = top 317 | newOptions.right = right 318 | newOptions.bottom = bottom 319 | newOptions.left = left 320 | // Account for defined browser orientation: 321 | switch (orientation) { 322 | case 2: 323 | // Horizontal flip 324 | newOptions.right = left 325 | newOptions.left = right 326 | break 327 | case 3: 328 | // 180° Rotate CCW 329 | newOptions.top = bottom 330 | newOptions.right = left 331 | newOptions.bottom = top 332 | newOptions.left = right 333 | break 334 | case 4: 335 | // Vertical flip 336 | newOptions.top = bottom 337 | newOptions.bottom = top 338 | break 339 | case 5: 340 | // Vertical flip + 90° Rotate CW 341 | newOptions.top = left 342 | newOptions.right = bottom 343 | newOptions.bottom = right 344 | newOptions.left = top 345 | break 346 | case 6: 347 | // 90° Rotate CW 348 | newOptions.top = right 349 | newOptions.right = bottom 350 | newOptions.bottom = left 351 | newOptions.left = top 352 | break 353 | case 7: 354 | // Horizontal flip + 90° Rotate CW 355 | newOptions.top = right 356 | newOptions.right = top 357 | newOptions.bottom = left 358 | newOptions.left = bottom 359 | break 360 | case 8: 361 | // 90° Rotate CCW 362 | newOptions.top = left 363 | newOptions.right = top 364 | newOptions.bottom = right 365 | newOptions.left = bottom 366 | break 367 | } 368 | return newOptions 369 | } 370 | 371 | // Transform image orientation based on the given EXIF orientation option: 372 | loadImage.transformCoordinates = function (canvas, options, data) { 373 | originalTransformCoordinates.call(loadImage, canvas, options, data) 374 | var orientation = options.orientation 375 | var autoOrientation = 376 | loadImage.orientation && data.exif && data.exif.get('Orientation') 377 | if (!requiresOrientationChange(orientation, autoOrientation)) { 378 | return 379 | } 380 | var ctx = canvas.getContext('2d') 381 | var width = canvas.width 382 | var height = canvas.height 383 | var sourceWidth = width 384 | var sourceHeight = height 385 | if ( 386 | (orientation > 4 && !(autoOrientation > 4)) || 387 | (orientation < 5 && autoOrientation > 4) 388 | ) { 389 | // Image dimensions and target dimensions are switched 390 | canvas.width = height 391 | canvas.height = width 392 | } 393 | if (orientation > 4) { 394 | // Destination and source dimensions are switched 395 | sourceWidth = height 396 | sourceHeight = width 397 | } 398 | // Reset automatic browser orientation: 399 | switch (autoOrientation) { 400 | case 2: 401 | // Horizontal flip 402 | ctx.translate(sourceWidth, 0) 403 | ctx.scale(-1, 1) 404 | break 405 | case 3: 406 | // 180° Rotate CCW 407 | ctx.translate(sourceWidth, sourceHeight) 408 | ctx.rotate(Math.PI) 409 | break 410 | case 4: 411 | // Vertical flip 412 | ctx.translate(0, sourceHeight) 413 | ctx.scale(1, -1) 414 | break 415 | case 5: 416 | // Horizontal flip + 90° Rotate CCW 417 | ctx.rotate(-0.5 * Math.PI) 418 | ctx.scale(-1, 1) 419 | break 420 | case 6: 421 | // 90° Rotate CCW 422 | ctx.rotate(-0.5 * Math.PI) 423 | ctx.translate(-sourceWidth, 0) 424 | break 425 | case 7: 426 | // Vertical flip + 90° Rotate CCW 427 | ctx.rotate(-0.5 * Math.PI) 428 | ctx.translate(-sourceWidth, sourceHeight) 429 | ctx.scale(1, -1) 430 | break 431 | case 8: 432 | // 90° Rotate CW 433 | ctx.rotate(0.5 * Math.PI) 434 | ctx.translate(0, -sourceHeight) 435 | break 436 | } 437 | // Some orientation combinations require additional rotation by 180°: 438 | if (requiresRot180(orientation, autoOrientation)) { 439 | ctx.translate(sourceWidth, sourceHeight) 440 | ctx.rotate(Math.PI) 441 | } 442 | switch (orientation) { 443 | case 2: 444 | // Horizontal flip 445 | ctx.translate(width, 0) 446 | ctx.scale(-1, 1) 447 | break 448 | case 3: 449 | // 180° Rotate CCW 450 | ctx.translate(width, height) 451 | ctx.rotate(Math.PI) 452 | break 453 | case 4: 454 | // Vertical flip 455 | ctx.translate(0, height) 456 | ctx.scale(1, -1) 457 | break 458 | case 5: 459 | // Vertical flip + 90° Rotate CW 460 | ctx.rotate(0.5 * Math.PI) 461 | ctx.scale(1, -1) 462 | break 463 | case 6: 464 | // 90° Rotate CW 465 | ctx.rotate(0.5 * Math.PI) 466 | ctx.translate(0, -height) 467 | break 468 | case 7: 469 | // Horizontal flip + 90° Rotate CW 470 | ctx.rotate(0.5 * Math.PI) 471 | ctx.translate(width, -height) 472 | ctx.scale(-1, 1) 473 | break 474 | case 8: 475 | // 90° Rotate CCW 476 | ctx.rotate(-0.5 * Math.PI) 477 | ctx.translate(-width, 0) 478 | break 479 | } 480 | } 481 | }) 482 | -------------------------------------------------------------------------------- /js/load-image-scale.js: -------------------------------------------------------------------------------- 1 | /* 2 | * JavaScript Load Image Scaling 3 | * https://github.com/blueimp/JavaScript-Load-Image 4 | * 5 | * Copyright 2011, Sebastian Tschan 6 | * https://blueimp.net 7 | * 8 | * Licensed under the MIT license: 9 | * https://opensource.org/licenses/MIT 10 | */ 11 | 12 | /* global define, module, require */ 13 | 14 | ;(function (factory) { 15 | 'use strict' 16 | if (typeof define === 'function' && define.amd) { 17 | // Register as an anonymous AMD module: 18 | define(['./load-image'], factory) 19 | } else if (typeof module === 'object' && module.exports) { 20 | factory(require('./load-image')) 21 | } else { 22 | // Browser globals: 23 | factory(window.loadImage) 24 | } 25 | })(function (loadImage) { 26 | 'use strict' 27 | 28 | var originalTransform = loadImage.transform 29 | 30 | loadImage.createCanvas = function (width, height, offscreen) { 31 | if (offscreen && loadImage.global.OffscreenCanvas) { 32 | return new OffscreenCanvas(width, height) 33 | } 34 | var canvas = document.createElement('canvas') 35 | canvas.width = width 36 | canvas.height = height 37 | return canvas 38 | } 39 | 40 | loadImage.transform = function (img, options, callback, file, data) { 41 | originalTransform.call( 42 | loadImage, 43 | loadImage.scale(img, options, data), 44 | options, 45 | callback, 46 | file, 47 | data 48 | ) 49 | } 50 | 51 | // Transform image coordinates, allows to override e.g. 52 | // the canvas orientation based on the orientation option, 53 | // gets canvas, options and data passed as arguments: 54 | loadImage.transformCoordinates = function () {} 55 | 56 | // Returns transformed options, allows to override e.g. 57 | // maxWidth, maxHeight and crop options based on the aspectRatio. 58 | // gets img, options, data passed as arguments: 59 | loadImage.getTransformedOptions = function (img, options) { 60 | var aspectRatio = options.aspectRatio 61 | var newOptions 62 | var i 63 | var width 64 | var height 65 | if (!aspectRatio) { 66 | return options 67 | } 68 | newOptions = {} 69 | for (i in options) { 70 | if (Object.prototype.hasOwnProperty.call(options, i)) { 71 | newOptions[i] = options[i] 72 | } 73 | } 74 | newOptions.crop = true 75 | width = img.naturalWidth || img.width 76 | height = img.naturalHeight || img.height 77 | if (width / height > aspectRatio) { 78 | newOptions.maxWidth = height * aspectRatio 79 | newOptions.maxHeight = height 80 | } else { 81 | newOptions.maxWidth = width 82 | newOptions.maxHeight = width / aspectRatio 83 | } 84 | return newOptions 85 | } 86 | 87 | // Canvas render method, allows to implement a different rendering algorithm: 88 | loadImage.drawImage = function ( 89 | img, 90 | canvas, 91 | sourceX, 92 | sourceY, 93 | sourceWidth, 94 | sourceHeight, 95 | destWidth, 96 | destHeight, 97 | options 98 | ) { 99 | var ctx = canvas.getContext('2d') 100 | if (options.imageSmoothingEnabled === false) { 101 | ctx.msImageSmoothingEnabled = false 102 | ctx.imageSmoothingEnabled = false 103 | } else if (options.imageSmoothingQuality) { 104 | ctx.imageSmoothingQuality = options.imageSmoothingQuality 105 | } 106 | ctx.drawImage( 107 | img, 108 | sourceX, 109 | sourceY, 110 | sourceWidth, 111 | sourceHeight, 112 | 0, 113 | 0, 114 | destWidth, 115 | destHeight 116 | ) 117 | return ctx 118 | } 119 | 120 | // Determines if the target image should be a canvas element: 121 | loadImage.requiresCanvas = function (options) { 122 | return options.canvas || options.crop || !!options.aspectRatio 123 | } 124 | 125 | // Scales and/or crops the given image (img or canvas HTML element) 126 | // using the given options: 127 | loadImage.scale = function (img, options, data) { 128 | // eslint-disable-next-line no-param-reassign 129 | options = options || {} 130 | // eslint-disable-next-line no-param-reassign 131 | data = data || {} 132 | var useCanvas = 133 | img.getContext || 134 | (loadImage.requiresCanvas(options) && 135 | !!loadImage.global.HTMLCanvasElement) 136 | var width = img.naturalWidth || img.width 137 | var height = img.naturalHeight || img.height 138 | var destWidth = width 139 | var destHeight = height 140 | var maxWidth 141 | var maxHeight 142 | var minWidth 143 | var minHeight 144 | var sourceWidth 145 | var sourceHeight 146 | var sourceX 147 | var sourceY 148 | var pixelRatio 149 | var downsamplingRatio 150 | var tmp 151 | var canvas 152 | /** 153 | * Scales up image dimensions 154 | */ 155 | function scaleUp() { 156 | var scale = Math.max( 157 | (minWidth || destWidth) / destWidth, 158 | (minHeight || destHeight) / destHeight 159 | ) 160 | if (scale > 1) { 161 | destWidth *= scale 162 | destHeight *= scale 163 | } 164 | } 165 | /** 166 | * Scales down image dimensions 167 | */ 168 | function scaleDown() { 169 | var scale = Math.min( 170 | (maxWidth || destWidth) / destWidth, 171 | (maxHeight || destHeight) / destHeight 172 | ) 173 | if (scale < 1) { 174 | destWidth *= scale 175 | destHeight *= scale 176 | } 177 | } 178 | if (useCanvas) { 179 | // eslint-disable-next-line no-param-reassign 180 | options = loadImage.getTransformedOptions(img, options, data) 181 | sourceX = options.left || 0 182 | sourceY = options.top || 0 183 | if (options.sourceWidth) { 184 | sourceWidth = options.sourceWidth 185 | if (options.right !== undefined && options.left === undefined) { 186 | sourceX = width - sourceWidth - options.right 187 | } 188 | } else { 189 | sourceWidth = width - sourceX - (options.right || 0) 190 | } 191 | if (options.sourceHeight) { 192 | sourceHeight = options.sourceHeight 193 | if (options.bottom !== undefined && options.top === undefined) { 194 | sourceY = height - sourceHeight - options.bottom 195 | } 196 | } else { 197 | sourceHeight = height - sourceY - (options.bottom || 0) 198 | } 199 | destWidth = sourceWidth 200 | destHeight = sourceHeight 201 | } 202 | maxWidth = options.maxWidth 203 | maxHeight = options.maxHeight 204 | minWidth = options.minWidth 205 | minHeight = options.minHeight 206 | if (useCanvas && maxWidth && maxHeight && options.crop) { 207 | destWidth = maxWidth 208 | destHeight = maxHeight 209 | tmp = sourceWidth / sourceHeight - maxWidth / maxHeight 210 | if (tmp < 0) { 211 | sourceHeight = (maxHeight * sourceWidth) / maxWidth 212 | if (options.top === undefined && options.bottom === undefined) { 213 | sourceY = (height - sourceHeight) / 2 214 | } 215 | } else if (tmp > 0) { 216 | sourceWidth = (maxWidth * sourceHeight) / maxHeight 217 | if (options.left === undefined && options.right === undefined) { 218 | sourceX = (width - sourceWidth) / 2 219 | } 220 | } 221 | } else { 222 | if (options.contain || options.cover) { 223 | minWidth = maxWidth = maxWidth || minWidth 224 | minHeight = maxHeight = maxHeight || minHeight 225 | } 226 | if (options.cover) { 227 | scaleDown() 228 | scaleUp() 229 | } else { 230 | scaleUp() 231 | scaleDown() 232 | } 233 | } 234 | if (useCanvas) { 235 | pixelRatio = options.pixelRatio 236 | if ( 237 | pixelRatio > 1 && 238 | // Check if the image has not yet had the device pixel ratio applied: 239 | !( 240 | img.style.width && 241 | Math.floor(parseFloat(img.style.width, 10)) === 242 | Math.floor(width / pixelRatio) 243 | ) 244 | ) { 245 | destWidth *= pixelRatio 246 | destHeight *= pixelRatio 247 | } 248 | // Check if workaround for Chromium orientation crop bug is required: 249 | // https://bugs.chromium.org/p/chromium/issues/detail?id=1074354 250 | if ( 251 | loadImage.orientationCropBug && 252 | !img.getContext && 253 | (sourceX || sourceY || sourceWidth !== width || sourceHeight !== height) 254 | ) { 255 | // Write the complete source image to an intermediate canvas first: 256 | tmp = img 257 | // eslint-disable-next-line no-param-reassign 258 | img = loadImage.createCanvas(width, height, true) 259 | loadImage.drawImage( 260 | tmp, 261 | img, 262 | 0, 263 | 0, 264 | width, 265 | height, 266 | width, 267 | height, 268 | options 269 | ) 270 | } 271 | downsamplingRatio = options.downsamplingRatio 272 | if ( 273 | downsamplingRatio > 0 && 274 | downsamplingRatio < 1 && 275 | destWidth < sourceWidth && 276 | destHeight < sourceHeight 277 | ) { 278 | while (sourceWidth * downsamplingRatio > destWidth) { 279 | canvas = loadImage.createCanvas( 280 | sourceWidth * downsamplingRatio, 281 | sourceHeight * downsamplingRatio, 282 | true 283 | ) 284 | loadImage.drawImage( 285 | img, 286 | canvas, 287 | sourceX, 288 | sourceY, 289 | sourceWidth, 290 | sourceHeight, 291 | canvas.width, 292 | canvas.height, 293 | options 294 | ) 295 | sourceX = 0 296 | sourceY = 0 297 | sourceWidth = canvas.width 298 | sourceHeight = canvas.height 299 | // eslint-disable-next-line no-param-reassign 300 | img = canvas 301 | } 302 | } 303 | canvas = loadImage.createCanvas(destWidth, destHeight) 304 | loadImage.transformCoordinates(canvas, options, data) 305 | if (pixelRatio > 1) { 306 | canvas.style.width = canvas.width / pixelRatio + 'px' 307 | } 308 | loadImage 309 | .drawImage( 310 | img, 311 | canvas, 312 | sourceX, 313 | sourceY, 314 | sourceWidth, 315 | sourceHeight, 316 | destWidth, 317 | destHeight, 318 | options 319 | ) 320 | .setTransform(1, 0, 0, 1, 0, 0) // reset to the identity matrix 321 | return canvas 322 | } 323 | img.width = destWidth 324 | img.height = destHeight 325 | return img 326 | } 327 | }) 328 | -------------------------------------------------------------------------------- /js/load-image.all.min.js: -------------------------------------------------------------------------------- 1 | !function(c){"use strict";var t=c.URL||c.webkitURL;function f(e){return!!t&&t.createObjectURL(e)}function i(e){return!!t&&t.revokeObjectURL(e)}function u(e,t){!e||"blob:"!==e.slice(0,5)||t&&t.noRevoke||i(e)}function d(e,t,i,a){if(!c.FileReader)return!1;var n=new FileReader;n.onload=function(){t.call(n,this.result)},i&&(n.onabort=n.onerror=function(){i.call(n,this.error)});a=n[a||"readAsDataURL"];return a?(a.call(n,e),n):void 0}function g(e,t){return Object.prototype.toString.call(t)==="[object "+e+"]"}function m(s,e,l){function t(i,a){var n,r=document.createElement("img");function o(e,t){i!==a?e instanceof Error?a(e):((t=t||{}).image=e,i(t)):i&&i(e,t)}function e(e,t){t&&c.console&&console.log(t),e&&g("Blob",e)?n=f(s=e):(n=s,l&&l.crossOrigin&&(r.crossOrigin=l.crossOrigin)),r.src=n}return r.onerror=function(e){u(n,l),a&&a.call(r,e)},r.onload=function(){u(n,l);var e={originalWidth:r.naturalWidth||r.width,originalHeight:r.naturalHeight||r.height};try{m.transform(r,l,o,s,e)}catch(t){a&&a(t)}},"string"==typeof s?(m.requiresMetaData(l)?m.fetchBlob(s,e,l):e(),r):g("Blob",s)||g("File",s)?(n=f(s))?(r.src=n,r):d(s,function(e){r.src=e},a):void 0}return c.Promise&&"function"!=typeof e?(l=e,new Promise(t)):t(e,e)}m.requiresMetaData=function(e){return e&&e.meta},m.fetchBlob=function(e,t){t()},m.transform=function(e,t,i,a,n){i(e,n)},m.global=c,m.readFile=d,m.isInstanceOf=g,m.createObjectURL=f,m.revokeObjectURL=i,"function"==typeof define&&define.amd?define(function(){return m}):"object"==typeof module&&module.exports?module.exports=m:c.loadImage=m}("undefined"!=typeof window&&window||this),function(e){"use strict";"function"==typeof define&&define.amd?define(["./load-image"],e):"object"==typeof module&&module.exports?e(require("./load-image")):e(window.loadImage)}(function(E){"use strict";var r=E.transform;E.createCanvas=function(e,t,i){if(i&&E.global.OffscreenCanvas)return new OffscreenCanvas(e,t);i=document.createElement("canvas");return i.width=e,i.height=t,i},E.transform=function(e,t,i,a,n){r.call(E,E.scale(e,t,n),t,i,a,n)},E.transformCoordinates=function(){},E.getTransformedOptions=function(e,t){var i,a,n,r=t.aspectRatio;if(!r)return t;for(a in i={},t)Object.prototype.hasOwnProperty.call(t,a)&&(i[a]=t[a]);return i.crop=!0,r<(n=e.naturalWidth||e.width)/(e=e.naturalHeight||e.height)?(i.maxWidth=e*r,i.maxHeight=e):(i.maxWidth=n,i.maxHeight=n/r),i},E.drawImage=function(e,t,i,a,n,r,o,s,l){t=t.getContext("2d");return!1===l.imageSmoothingEnabled?(t.msImageSmoothingEnabled=!1,t.imageSmoothingEnabled=!1):l.imageSmoothingQuality&&(t.imageSmoothingQuality=l.imageSmoothingQuality),t.drawImage(e,i,a,n,r,0,0,o,s),t},E.requiresCanvas=function(e){return e.canvas||e.crop||!!e.aspectRatio},E.scale=function(e,t,i){t=t||{},i=i||{};var a,n,r,o,s,l,c,f,u,d,g,m=e.getContext||E.requiresCanvas(t)&&!!E.global.HTMLCanvasElement,h=e.naturalWidth||e.width,p=e.naturalHeight||e.height,A=h,b=p;function y(){var e=Math.max((r||A)/A,(o||b)/b);1t.byteLength){console.log("Invalid JPEG metadata: Invalid segment size.");break}if((n=h.jpeg[i])&&!u.disableMetaDataParsers)for(r=0;re.byteLength)console.log("Invalid Exif data: Invalid directory offset.");else{if(!((c=i+2+12*(l=e.getUint16(i,a)))+4>e.byteLength)){for(f=0;fe.byteLength)){if(1===n)return u.getValue(e,o,r);for(s=[],l=0;ll.byteLength)console.log("Invalid Exif data: Invalid segment size.");else if(0===l.getUint16(e+8)){switch(l.getUint16(g)){case 18761:f=!0;break;case 19789:f=!1;break;default:return void console.log("Invalid Exif data: Invalid byte alignment marker.")}42===l.getUint16(g+2,f)?(e=l.getUint32(g+4,f),c.exif=new m,i.disableExifOffsets||(c.exifOffsets=new m,c.exifTiffOffset=g,c.exifLittleEndian=f),(e=A(l,g,g+e,f,c.exif,c.exifOffsets,u,d))&&p(u,d,"ifd1")&&(c.exif.ifd1=e,c.exifOffsets&&(c.exifOffsets.ifd1=g+e)),Object.keys(c.exif.ifds).forEach(function(e){var t,i,a,n,r,o,s;i=e,a=l,n=g,r=f,o=u,s=d,(e=(t=c).exif[i])&&(t.exif[i]=new m(i),t.exifOffsets&&(t.exifOffsets[i]=new m(i)),A(a,n,n+e,r,t.exif[i],t.exifOffsets&&t.exifOffsets[i],o&&o[i],s&&s[i]))}),(e=c.exif.ifd1)&&e[513]&&(e[513]=function(e,t,i){if(i){if(!(t+i>e.byteLength))return new Blob([n.bufferSlice.call(e.buffer,t,t+i)],{type:"image/jpeg"});console.log("Invalid Exif data: Invalid thumbnail data.")}}(l,g+e[513],e[514]))):console.log("Invalid Exif data: Missing TIFF marker.")}else console.log("Invalid Exif data: Missing byte alignment offset.")}},n.metaDataParsers.jpeg[65505].push(n.parseExifData),n.exifWriters={274:function(e,t,i){var a=t.exifOffsets[274];return a&&new DataView(e,a+8,2).setUint16(0,i,t.exifLittleEndian),e}},n.writeExifData=function(e,t,i,a){return n.exifWriters[t.exif.map[i]](e,t,a)},n.ExifMap=m}),function(e){"use strict";"function"==typeof define&&define.amd?define(["./load-image","./load-image-exif"],e):"object"==typeof module&&module.exports?e(require("./load-image"),require("./load-image-exif")):e(window.loadImage)}(function(e){"use strict";var n=e.ExifMap.prototype;n.tags={256:"ImageWidth",257:"ImageHeight",258:"BitsPerSample",259:"Compression",262:"PhotometricInterpretation",274:"Orientation",277:"SamplesPerPixel",284:"PlanarConfiguration",530:"YCbCrSubSampling",531:"YCbCrPositioning",282:"XResolution",283:"YResolution",296:"ResolutionUnit",273:"StripOffsets",278:"RowsPerStrip",279:"StripByteCounts",513:"JPEGInterchangeFormat",514:"JPEGInterchangeFormatLength",301:"TransferFunction",318:"WhitePoint",319:"PrimaryChromaticities",529:"YCbCrCoefficients",532:"ReferenceBlackWhite",306:"DateTime",270:"ImageDescription",271:"Make",272:"Model",305:"Software",315:"Artist",33432:"Copyright",34665:{36864:"ExifVersion",40960:"FlashpixVersion",40961:"ColorSpace",40962:"PixelXDimension",40963:"PixelYDimension",42240:"Gamma",37121:"ComponentsConfiguration",37122:"CompressedBitsPerPixel",37500:"MakerNote",37510:"UserComment",40964:"RelatedSoundFile",36867:"DateTimeOriginal",36868:"DateTimeDigitized",36880:"OffsetTime",36881:"OffsetTimeOriginal",36882:"OffsetTimeDigitized",37520:"SubSecTime",37521:"SubSecTimeOriginal",37522:"SubSecTimeDigitized",33434:"ExposureTime",33437:"FNumber",34850:"ExposureProgram",34852:"SpectralSensitivity",34855:"PhotographicSensitivity",34856:"OECF",34864:"SensitivityType",34865:"StandardOutputSensitivity",34866:"RecommendedExposureIndex",34867:"ISOSpeed",34868:"ISOSpeedLatitudeyyy",34869:"ISOSpeedLatitudezzz",37377:"ShutterSpeedValue",37378:"ApertureValue",37379:"BrightnessValue",37380:"ExposureBias",37381:"MaxApertureValue",37382:"SubjectDistance",37383:"MeteringMode",37384:"LightSource",37385:"Flash",37396:"SubjectArea",37386:"FocalLength",41483:"FlashEnergy",41484:"SpatialFrequencyResponse",41486:"FocalPlaneXResolution",41487:"FocalPlaneYResolution",41488:"FocalPlaneResolutionUnit",41492:"SubjectLocation",41493:"ExposureIndex",41495:"SensingMethod",41728:"FileSource",41729:"SceneType",41730:"CFAPattern",41985:"CustomRendered",41986:"ExposureMode",41987:"WhiteBalance",41988:"DigitalZoomRatio",41989:"FocalLengthIn35mmFilm",41990:"SceneCaptureType",41991:"GainControl",41992:"Contrast",41993:"Saturation",41994:"Sharpness",41995:"DeviceSettingDescription",41996:"SubjectDistanceRange",42016:"ImageUniqueID",42032:"CameraOwnerName",42033:"BodySerialNumber",42034:"LensSpecification",42035:"LensMake",42036:"LensModel",42037:"LensSerialNumber"},34853:{0:"GPSVersionID",1:"GPSLatitudeRef",2:"GPSLatitude",3:"GPSLongitudeRef",4:"GPSLongitude",5:"GPSAltitudeRef",6:"GPSAltitude",7:"GPSTimeStamp",8:"GPSSatellites",9:"GPSStatus",10:"GPSMeasureMode",11:"GPSDOP",12:"GPSSpeedRef",13:"GPSSpeed",14:"GPSTrackRef",15:"GPSTrack",16:"GPSImgDirectionRef",17:"GPSImgDirection",18:"GPSMapDatum",19:"GPSDestLatitudeRef",20:"GPSDestLatitude",21:"GPSDestLongitudeRef",22:"GPSDestLongitude",23:"GPSDestBearingRef",24:"GPSDestBearing",25:"GPSDestDistanceRef",26:"GPSDestDistance",27:"GPSProcessingMethod",28:"GPSAreaInformation",29:"GPSDateStamp",30:"GPSDifferential",31:"GPSHPositioningError"},40965:{1:"InteroperabilityIndex"}},n.tags.ifd1=n.tags,n.stringValues={ExposureProgram:{0:"Undefined",1:"Manual",2:"Normal program",3:"Aperture priority",4:"Shutter priority",5:"Creative program",6:"Action program",7:"Portrait mode",8:"Landscape mode"},MeteringMode:{0:"Unknown",1:"Average",2:"CenterWeightedAverage",3:"Spot",4:"MultiSpot",5:"Pattern",6:"Partial",255:"Other"},LightSource:{0:"Unknown",1:"Daylight",2:"Fluorescent",3:"Tungsten (incandescent light)",4:"Flash",9:"Fine weather",10:"Cloudy weather",11:"Shade",12:"Daylight fluorescent (D 5700 - 7100K)",13:"Day white fluorescent (N 4600 - 5400K)",14:"Cool white fluorescent (W 3900 - 4500K)",15:"White fluorescent (WW 3200 - 3700K)",17:"Standard light A",18:"Standard light B",19:"Standard light C",20:"D55",21:"D65",22:"D75",23:"D50",24:"ISO studio tungsten",255:"Other"},Flash:{0:"Flash did not fire",1:"Flash fired",5:"Strobe return light not detected",7:"Strobe return light detected",9:"Flash fired, compulsory flash mode",13:"Flash fired, compulsory flash mode, return light not detected",15:"Flash fired, compulsory flash mode, return light detected",16:"Flash did not fire, compulsory flash mode",24:"Flash did not fire, auto mode",25:"Flash fired, auto mode",29:"Flash fired, auto mode, return light not detected",31:"Flash fired, auto mode, return light detected",32:"No flash function",65:"Flash fired, red-eye reduction mode",69:"Flash fired, red-eye reduction mode, return light not detected",71:"Flash fired, red-eye reduction mode, return light detected",73:"Flash fired, compulsory flash mode, red-eye reduction mode",77:"Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected",79:"Flash fired, compulsory flash mode, red-eye reduction mode, return light detected",89:"Flash fired, auto mode, red-eye reduction mode",93:"Flash fired, auto mode, return light not detected, red-eye reduction mode",95:"Flash fired, auto mode, return light detected, red-eye reduction mode"},SensingMethod:{1:"Undefined",2:"One-chip color area sensor",3:"Two-chip color area sensor",4:"Three-chip color area sensor",5:"Color sequential area sensor",7:"Trilinear sensor",8:"Color sequential linear sensor"},SceneCaptureType:{0:"Standard",1:"Landscape",2:"Portrait",3:"Night scene"},SceneType:{1:"Directly photographed"},CustomRendered:{0:"Normal process",1:"Custom process"},WhiteBalance:{0:"Auto white balance",1:"Manual white balance"},GainControl:{0:"None",1:"Low gain up",2:"High gain up",3:"Low gain down",4:"High gain down"},Contrast:{0:"Normal",1:"Soft",2:"Hard"},Saturation:{0:"Normal",1:"Low saturation",2:"High saturation"},Sharpness:{0:"Normal",1:"Soft",2:"Hard"},SubjectDistanceRange:{0:"Unknown",1:"Macro",2:"Close view",3:"Distant view"},FileSource:{3:"DSC"},ComponentsConfiguration:{0:"",1:"Y",2:"Cb",3:"Cr",4:"R",5:"G",6:"B"},Orientation:{1:"Original",2:"Horizontal flip",3:"Rotate 180° CCW",4:"Vertical flip",5:"Vertical flip + Rotate 90° CW",6:"Rotate 90° CW",7:"Horizontal flip + Rotate 90° CW",8:"Rotate 90° CCW"}},n.getText=function(e){var t=this.get(e);switch(e){case"LightSource":case"Flash":case"MeteringMode":case"ExposureProgram":case"SensingMethod":case"SceneCaptureType":case"SceneType":case"CustomRendered":case"WhiteBalance":case"GainControl":case"Contrast":case"Saturation":case"Sharpness":case"SubjectDistanceRange":case"FileSource":case"Orientation":return this.stringValues[e][t];case"ExifVersion":case"FlashpixVersion":return t?String.fromCharCode(t[0],t[1],t[2],t[3]):void 0;case"ComponentsConfiguration":return t?this.stringValues[e][t[0]]+this.stringValues[e][t[1]]+this.stringValues[e][t[2]]+this.stringValues[e][t[3]]:void 0;case"GPSVersionID":return t?t[0]+"."+t[1]+"."+t[2]+"."+t[3]:void 0}return String(t)},n.getAll=function(){var e,t,i={};for(e in this)Object.prototype.hasOwnProperty.call(this,e)&&((t=this[e])&&t.getAll?i[this.ifds[e].name]=t.getAll():(t=this.tags[e])&&(i[t]=this.getText(t)));return i},n.getName=function(e){var t=this.tags[e];return"object"==typeof t?this.ifds[e].name:t},function(){var e,t,i,a=n.tags;for(e in a)if(Object.prototype.hasOwnProperty.call(a,e))if(t=n.ifds[e])for(e in i=a[e])Object.prototype.hasOwnProperty.call(i,e)&&(t.map[i[e]]=Number(e));else n.map[a[e]]=Number(e)}()}),function(e){"use strict";"function"==typeof define&&define.amd?define(["./load-image","./load-image-meta"],e):"object"==typeof module&&module.exports?e(require("./load-image"),require("./load-image-meta")):e(window.loadImage)}(function(e){"use strict";function l(){}function u(e,t,i,a,n){return"binary"===t.types[e]?new Blob([i.buffer.slice(a,a+n)]):"Uint16"===t.types[e]?i.getUint16(a):function(e,t,i){for(var a="",n=t+i,r=t;r} Object 101 | */ 102 | function loadImage(file, callback, options) { 103 | /** 104 | * Promise executor 105 | * 106 | * @param {Function} resolve Resolution function 107 | * @param {Function} reject Rejection function 108 | * @returns {HTMLImageElement|FileReader} Object 109 | */ 110 | function executor(resolve, reject) { 111 | var img = document.createElement('img') 112 | var url 113 | /** 114 | * Callback for the fetchBlob call. 115 | * 116 | * @param {HTMLImageElement|HTMLCanvasElement} img Error object 117 | * @param {object} data Data object 118 | * @returns {undefined} Undefined 119 | */ 120 | function resolveWrapper(img, data) { 121 | if (resolve === reject) { 122 | // Not using Promises 123 | if (resolve) resolve(img, data) 124 | return 125 | } else if (img instanceof Error) { 126 | reject(img) 127 | return 128 | } 129 | data = data || {} // eslint-disable-line no-param-reassign 130 | data.image = img 131 | resolve(data) 132 | } 133 | /** 134 | * Callback for the fetchBlob call. 135 | * 136 | * @param {Blob} blob Blob object 137 | * @param {Error} err Error object 138 | */ 139 | function fetchBlobCallback(blob, err) { 140 | if (err && $.console) console.log(err) // eslint-disable-line no-console 141 | if (blob && isInstanceOf('Blob', blob)) { 142 | file = blob // eslint-disable-line no-param-reassign 143 | url = createObjectURL(file) 144 | } else { 145 | url = file 146 | if (options && options.crossOrigin) { 147 | img.crossOrigin = options.crossOrigin 148 | } 149 | } 150 | img.src = url 151 | } 152 | img.onerror = function (event) { 153 | revokeHelper(url, options) 154 | if (reject) reject.call(img, event) 155 | } 156 | img.onload = function () { 157 | revokeHelper(url, options) 158 | var data = { 159 | originalWidth: img.naturalWidth || img.width, 160 | originalHeight: img.naturalHeight || img.height 161 | } 162 | try { 163 | loadImage.transform(img, options, resolveWrapper, file, data) 164 | } catch (error) { 165 | if (reject) reject(error) 166 | } 167 | } 168 | if (typeof file === 'string') { 169 | if (loadImage.requiresMetaData(options)) { 170 | loadImage.fetchBlob(file, fetchBlobCallback, options) 171 | } else { 172 | fetchBlobCallback() 173 | } 174 | return img 175 | } else if (isInstanceOf('Blob', file) || isInstanceOf('File', file)) { 176 | url = createObjectURL(file) 177 | if (url) { 178 | img.src = url 179 | return img 180 | } 181 | return readFile( 182 | file, 183 | function (url) { 184 | img.src = url 185 | }, 186 | reject 187 | ) 188 | } 189 | } 190 | if ($.Promise && typeof callback !== 'function') { 191 | options = callback // eslint-disable-line no-param-reassign 192 | return new Promise(executor) 193 | } 194 | return executor(callback, callback) 195 | } 196 | 197 | // Determines if metadata should be loaded automatically. 198 | // Requires the load image meta extension to load metadata. 199 | loadImage.requiresMetaData = function (options) { 200 | return options && options.meta 201 | } 202 | 203 | // If the callback given to this function returns a blob, it is used as image 204 | // source instead of the original url and overrides the file argument used in 205 | // the onload and onerror event callbacks: 206 | loadImage.fetchBlob = function (url, callback) { 207 | callback() 208 | } 209 | 210 | loadImage.transform = function (img, options, callback, file, data) { 211 | callback(img, data) 212 | } 213 | 214 | loadImage.global = $ 215 | loadImage.readFile = readFile 216 | loadImage.isInstanceOf = isInstanceOf 217 | loadImage.createObjectURL = createObjectURL 218 | loadImage.revokeObjectURL = revokeObjectURL 219 | 220 | if (typeof define === 'function' && define.amd) { 221 | define(function () { 222 | return loadImage 223 | }) 224 | } else if (typeof module === 'object' && module.exports) { 225 | module.exports = loadImage 226 | } else { 227 | $.loadImage = loadImage 228 | } 229 | })((typeof window !== 'undefined' && window) || this) 230 | -------------------------------------------------------------------------------- /js/vendor/canvas-to-blob.js: -------------------------------------------------------------------------------- 1 | /* 2 | * JavaScript Canvas to Blob 3 | * https://github.com/blueimp/JavaScript-Canvas-to-Blob 4 | * 5 | * Copyright 2012, Sebastian Tschan 6 | * https://blueimp.net 7 | * 8 | * Licensed under the MIT license: 9 | * https://opensource.org/licenses/MIT 10 | * 11 | * Based on stackoverflow user Stoive's code snippet: 12 | * http://stackoverflow.com/q/4998908 13 | */ 14 | 15 | /* global define, Uint8Array, ArrayBuffer, module */ 16 | 17 | ;(function (window) { 18 | 'use strict' 19 | 20 | var CanvasPrototype = 21 | window.HTMLCanvasElement && window.HTMLCanvasElement.prototype 22 | var hasBlobConstructor = 23 | window.Blob && 24 | (function () { 25 | try { 26 | return Boolean(new Blob()) 27 | } catch (e) { 28 | return false 29 | } 30 | })() 31 | var hasArrayBufferViewSupport = 32 | hasBlobConstructor && 33 | window.Uint8Array && 34 | (function () { 35 | try { 36 | return new Blob([new Uint8Array(100)]).size === 100 37 | } catch (e) { 38 | return false 39 | } 40 | })() 41 | var BlobBuilder = 42 | window.BlobBuilder || 43 | window.WebKitBlobBuilder || 44 | window.MozBlobBuilder || 45 | window.MSBlobBuilder 46 | var dataURIPattern = /^data:((.*?)(;charset=.*?)?)(;base64)?,/ 47 | var dataURLtoBlob = 48 | (hasBlobConstructor || BlobBuilder) && 49 | window.atob && 50 | window.ArrayBuffer && 51 | window.Uint8Array && 52 | function (dataURI) { 53 | var matches, 54 | mediaType, 55 | isBase64, 56 | dataString, 57 | byteString, 58 | arrayBuffer, 59 | intArray, 60 | i, 61 | bb 62 | // Parse the dataURI components as per RFC 2397 63 | matches = dataURI.match(dataURIPattern) 64 | if (!matches) { 65 | throw new Error('invalid data URI') 66 | } 67 | // Default to text/plain;charset=US-ASCII 68 | mediaType = matches[2] 69 | ? matches[1] 70 | : 'text/plain' + (matches[3] || ';charset=US-ASCII') 71 | isBase64 = !!matches[4] 72 | dataString = dataURI.slice(matches[0].length) 73 | if (isBase64) { 74 | // Convert base64 to raw binary data held in a string: 75 | byteString = atob(dataString) 76 | } else { 77 | // Convert base64/URLEncoded data component to raw binary: 78 | byteString = decodeURIComponent(dataString) 79 | } 80 | // Write the bytes of the string to an ArrayBuffer: 81 | arrayBuffer = new ArrayBuffer(byteString.length) 82 | intArray = new Uint8Array(arrayBuffer) 83 | for (i = 0; i < byteString.length; i += 1) { 84 | intArray[i] = byteString.charCodeAt(i) 85 | } 86 | // Write the ArrayBuffer (or ArrayBufferView) to a blob: 87 | if (hasBlobConstructor) { 88 | return new Blob([hasArrayBufferViewSupport ? intArray : arrayBuffer], { 89 | type: mediaType 90 | }) 91 | } 92 | bb = new BlobBuilder() 93 | bb.append(arrayBuffer) 94 | return bb.getBlob(mediaType) 95 | } 96 | if (window.HTMLCanvasElement && !CanvasPrototype.toBlob) { 97 | if (CanvasPrototype.mozGetAsFile) { 98 | CanvasPrototype.toBlob = function (callback, type, quality) { 99 | var self = this 100 | setTimeout(function () { 101 | if (quality && CanvasPrototype.toDataURL && dataURLtoBlob) { 102 | callback(dataURLtoBlob(self.toDataURL(type, quality))) 103 | } else { 104 | callback(self.mozGetAsFile('blob', type)) 105 | } 106 | }) 107 | } 108 | } else if (CanvasPrototype.toDataURL && dataURLtoBlob) { 109 | if (CanvasPrototype.msToBlob) { 110 | CanvasPrototype.toBlob = function (callback, type, quality) { 111 | var self = this 112 | setTimeout(function () { 113 | if ( 114 | ((type && type !== 'image/png') || quality) && 115 | CanvasPrototype.toDataURL && 116 | dataURLtoBlob 117 | ) { 118 | callback(dataURLtoBlob(self.toDataURL(type, quality))) 119 | } else { 120 | callback(self.msToBlob(type)) 121 | } 122 | }) 123 | } 124 | } else { 125 | CanvasPrototype.toBlob = function (callback, type, quality) { 126 | var self = this 127 | setTimeout(function () { 128 | callback(dataURLtoBlob(self.toDataURL(type, quality))) 129 | }) 130 | } 131 | } 132 | } 133 | } 134 | if (typeof define === 'function' && define.amd) { 135 | define(function () { 136 | return dataURLtoBlob 137 | }) 138 | } else if (typeof module === 'object' && module.exports) { 139 | module.exports = dataURLtoBlob 140 | } else { 141 | window.dataURLtoBlob = dataURLtoBlob 142 | } 143 | })(window) 144 | -------------------------------------------------------------------------------- /js/vendor/promise-polyfill.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? factory() : 3 | typeof define === 'function' && define.amd ? define(factory) : 4 | (factory()); 5 | }(this, (function () { 'use strict'; 6 | 7 | /** 8 | * @this {Promise} 9 | */ 10 | function finallyConstructor(callback) { 11 | var constructor = this.constructor; 12 | return this.then( 13 | function(value) { 14 | // @ts-ignore 15 | return constructor.resolve(callback()).then(function() { 16 | return value; 17 | }); 18 | }, 19 | function(reason) { 20 | // @ts-ignore 21 | return constructor.resolve(callback()).then(function() { 22 | // @ts-ignore 23 | return constructor.reject(reason); 24 | }); 25 | } 26 | ); 27 | } 28 | 29 | function allSettled(arr) { 30 | var P = this; 31 | return new P(function(resolve, reject) { 32 | if (!(arr && typeof arr.length !== 'undefined')) { 33 | return reject( 34 | new TypeError( 35 | typeof arr + 36 | ' ' + 37 | arr + 38 | ' is not iterable(cannot read property Symbol(Symbol.iterator))' 39 | ) 40 | ); 41 | } 42 | var args = Array.prototype.slice.call(arr); 43 | if (args.length === 0) return resolve([]); 44 | var remaining = args.length; 45 | 46 | function res(i, val) { 47 | if (val && (typeof val === 'object' || typeof val === 'function')) { 48 | var then = val.then; 49 | if (typeof then === 'function') { 50 | then.call( 51 | val, 52 | function(val) { 53 | res(i, val); 54 | }, 55 | function(e) { 56 | args[i] = { status: 'rejected', reason: e }; 57 | if (--remaining === 0) { 58 | resolve(args); 59 | } 60 | } 61 | ); 62 | return; 63 | } 64 | } 65 | args[i] = { status: 'fulfilled', value: val }; 66 | if (--remaining === 0) { 67 | resolve(args); 68 | } 69 | } 70 | 71 | for (var i = 0; i < args.length; i++) { 72 | res(i, args[i]); 73 | } 74 | }); 75 | } 76 | 77 | // Store setTimeout reference so promise-polyfill will be unaffected by 78 | // other code modifying setTimeout (like sinon.useFakeTimers()) 79 | var setTimeoutFunc = setTimeout; 80 | 81 | function isArray(x) { 82 | return Boolean(x && typeof x.length !== 'undefined'); 83 | } 84 | 85 | function noop() {} 86 | 87 | // Polyfill for Function.prototype.bind 88 | function bind(fn, thisArg) { 89 | return function() { 90 | fn.apply(thisArg, arguments); 91 | }; 92 | } 93 | 94 | /** 95 | * @constructor 96 | * @param {Function} fn 97 | */ 98 | function Promise(fn) { 99 | if (!(this instanceof Promise)) 100 | throw new TypeError('Promises must be constructed via new'); 101 | if (typeof fn !== 'function') throw new TypeError('not a function'); 102 | /** @type {!number} */ 103 | this._state = 0; 104 | /** @type {!boolean} */ 105 | this._handled = false; 106 | /** @type {Promise|undefined} */ 107 | this._value = undefined; 108 | /** @type {!Array} */ 109 | this._deferreds = []; 110 | 111 | doResolve(fn, this); 112 | } 113 | 114 | function handle(self, deferred) { 115 | while (self._state === 3) { 116 | self = self._value; 117 | } 118 | if (self._state === 0) { 119 | self._deferreds.push(deferred); 120 | return; 121 | } 122 | self._handled = true; 123 | Promise._immediateFn(function() { 124 | var cb = self._state === 1 ? deferred.onFulfilled : deferred.onRejected; 125 | if (cb === null) { 126 | (self._state === 1 ? resolve : reject)(deferred.promise, self._value); 127 | return; 128 | } 129 | var ret; 130 | try { 131 | ret = cb(self._value); 132 | } catch (e) { 133 | reject(deferred.promise, e); 134 | return; 135 | } 136 | resolve(deferred.promise, ret); 137 | }); 138 | } 139 | 140 | function resolve(self, newValue) { 141 | try { 142 | // Promise Resolution Procedure: https://github.com/promises-aplus/promises-spec#the-promise-resolution-procedure 143 | if (newValue === self) 144 | throw new TypeError('A promise cannot be resolved with itself.'); 145 | if ( 146 | newValue && 147 | (typeof newValue === 'object' || typeof newValue === 'function') 148 | ) { 149 | var then = newValue.then; 150 | if (newValue instanceof Promise) { 151 | self._state = 3; 152 | self._value = newValue; 153 | finale(self); 154 | return; 155 | } else if (typeof then === 'function') { 156 | doResolve(bind(then, newValue), self); 157 | return; 158 | } 159 | } 160 | self._state = 1; 161 | self._value = newValue; 162 | finale(self); 163 | } catch (e) { 164 | reject(self, e); 165 | } 166 | } 167 | 168 | function reject(self, newValue) { 169 | self._state = 2; 170 | self._value = newValue; 171 | finale(self); 172 | } 173 | 174 | function finale(self) { 175 | if (self._state === 2 && self._deferreds.length === 0) { 176 | Promise._immediateFn(function() { 177 | if (!self._handled) { 178 | Promise._unhandledRejectionFn(self._value); 179 | } 180 | }); 181 | } 182 | 183 | for (var i = 0, len = self._deferreds.length; i < len; i++) { 184 | handle(self, self._deferreds[i]); 185 | } 186 | self._deferreds = null; 187 | } 188 | 189 | /** 190 | * @constructor 191 | */ 192 | function Handler(onFulfilled, onRejected, promise) { 193 | this.onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : null; 194 | this.onRejected = typeof onRejected === 'function' ? onRejected : null; 195 | this.promise = promise; 196 | } 197 | 198 | /** 199 | * Take a potentially misbehaving resolver function and make sure 200 | * onFulfilled and onRejected are only called once. 201 | * 202 | * Makes no guarantees about asynchrony. 203 | */ 204 | function doResolve(fn, self) { 205 | var done = false; 206 | try { 207 | fn( 208 | function(value) { 209 | if (done) return; 210 | done = true; 211 | resolve(self, value); 212 | }, 213 | function(reason) { 214 | if (done) return; 215 | done = true; 216 | reject(self, reason); 217 | } 218 | ); 219 | } catch (ex) { 220 | if (done) return; 221 | done = true; 222 | reject(self, ex); 223 | } 224 | } 225 | 226 | Promise.prototype['catch'] = function(onRejected) { 227 | return this.then(null, onRejected); 228 | }; 229 | 230 | Promise.prototype.then = function(onFulfilled, onRejected) { 231 | // @ts-ignore 232 | var prom = new this.constructor(noop); 233 | 234 | handle(this, new Handler(onFulfilled, onRejected, prom)); 235 | return prom; 236 | }; 237 | 238 | Promise.prototype['finally'] = finallyConstructor; 239 | 240 | Promise.all = function(arr) { 241 | return new Promise(function(resolve, reject) { 242 | if (!isArray(arr)) { 243 | return reject(new TypeError('Promise.all accepts an array')); 244 | } 245 | 246 | var args = Array.prototype.slice.call(arr); 247 | if (args.length === 0) return resolve([]); 248 | var remaining = args.length; 249 | 250 | function res(i, val) { 251 | try { 252 | if (val && (typeof val === 'object' || typeof val === 'function')) { 253 | var then = val.then; 254 | if (typeof then === 'function') { 255 | then.call( 256 | val, 257 | function(val) { 258 | res(i, val); 259 | }, 260 | reject 261 | ); 262 | return; 263 | } 264 | } 265 | args[i] = val; 266 | if (--remaining === 0) { 267 | resolve(args); 268 | } 269 | } catch (ex) { 270 | reject(ex); 271 | } 272 | } 273 | 274 | for (var i = 0; i < args.length; i++) { 275 | res(i, args[i]); 276 | } 277 | }); 278 | }; 279 | 280 | Promise.allSettled = allSettled; 281 | 282 | Promise.resolve = function(value) { 283 | if (value && typeof value === 'object' && value.constructor === Promise) { 284 | return value; 285 | } 286 | 287 | return new Promise(function(resolve) { 288 | resolve(value); 289 | }); 290 | }; 291 | 292 | Promise.reject = function(value) { 293 | return new Promise(function(resolve, reject) { 294 | reject(value); 295 | }); 296 | }; 297 | 298 | Promise.race = function(arr) { 299 | return new Promise(function(resolve, reject) { 300 | if (!isArray(arr)) { 301 | return reject(new TypeError('Promise.race accepts an array')); 302 | } 303 | 304 | for (var i = 0, len = arr.length; i < len; i++) { 305 | Promise.resolve(arr[i]).then(resolve, reject); 306 | } 307 | }); 308 | }; 309 | 310 | // Use polyfill for setImmediate for performance gains 311 | Promise._immediateFn = 312 | // @ts-ignore 313 | (typeof setImmediate === 'function' && 314 | function(fn) { 315 | // @ts-ignore 316 | setImmediate(fn); 317 | }) || 318 | function(fn) { 319 | setTimeoutFunc(fn, 0); 320 | }; 321 | 322 | Promise._unhandledRejectionFn = function _unhandledRejectionFn(err) { 323 | if (typeof console !== 'undefined' && console) { 324 | console.warn('Possible Unhandled Promise Rejection:', err); // eslint-disable-line no-console 325 | } 326 | }; 327 | 328 | /** @suppress {undefinedVars} */ 329 | var globalNS = (function() { 330 | // the only reliable means to get the global object is 331 | // `Function('return this')()` 332 | // However, this causes CSP violations in Chrome apps. 333 | if (typeof self !== 'undefined') { 334 | return self; 335 | } 336 | if (typeof window !== 'undefined') { 337 | return window; 338 | } 339 | if (typeof global !== 'undefined') { 340 | return global; 341 | } 342 | throw new Error('unable to locate global object'); 343 | })(); 344 | 345 | // Expose the polyfill if Promise is undefined or set to a 346 | // non-function value. The latter can be due to a named HTMLElement 347 | // being exposed by browsers for legacy reasons. 348 | // https://github.com/taylorhakes/promise-polyfill/issues/114 349 | if (typeof globalNS['Promise'] !== 'function') { 350 | globalNS['Promise'] = Promise; 351 | } else if (!globalNS.Promise.prototype['finally']) { 352 | globalNS.Promise.prototype['finally'] = finallyConstructor; 353 | } else if (!globalNS.Promise.allSettled) { 354 | globalNS.Promise.allSettled = allSettled; 355 | } 356 | 357 | }))); 358 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blueimp-load-image", 3 | "version": "5.16.0", 4 | "title": "JavaScript Load Image", 5 | "description": "JavaScript Load Image is a library to load images provided as File or Blob objects or via URL. It returns an optionally scaled, cropped or rotated HTML img or canvas element. It also provides methods to parse image metadata to extract IPTC and Exif tags as well as embedded thumbnail images, to overwrite the Exif Orientation value and to restore the complete image header after resizing.", 6 | "keywords": [ 7 | "javascript", 8 | "load", 9 | "loading", 10 | "image", 11 | "file", 12 | "blob", 13 | "url", 14 | "scale", 15 | "crop", 16 | "rotate", 17 | "img", 18 | "canvas", 19 | "meta", 20 | "exif", 21 | "orientation", 22 | "thumbnail", 23 | "iptc" 24 | ], 25 | "homepage": "https://github.com/blueimp/JavaScript-Load-Image", 26 | "author": { 27 | "name": "Sebastian Tschan", 28 | "url": "https://blueimp.net" 29 | }, 30 | "repository": { 31 | "type": "git", 32 | "url": "git://github.com/blueimp/JavaScript-Load-Image.git" 33 | }, 34 | "license": "MIT", 35 | "devDependencies": { 36 | "blueimp-canvas-to-blob": "3", 37 | "chai": "4", 38 | "eslint": "7", 39 | "eslint-config-blueimp": "2", 40 | "eslint-config-prettier": "8", 41 | "eslint-plugin-jsdoc": "36", 42 | "eslint-plugin-prettier": "4", 43 | "jquery": "1", 44 | "mocha": "9", 45 | "prettier": "2", 46 | "promise-polyfill": "8", 47 | "uglify-js": "3" 48 | }, 49 | "eslintConfig": { 50 | "extends": [ 51 | "blueimp", 52 | "plugin:jsdoc/recommended", 53 | "plugin:prettier/recommended" 54 | ], 55 | "env": { 56 | "browser": true 57 | } 58 | }, 59 | "eslintIgnore": [ 60 | "js/*.min.js", 61 | "js/vendor", 62 | "test/vendor" 63 | ], 64 | "prettier": { 65 | "arrowParens": "avoid", 66 | "proseWrap": "always", 67 | "semi": false, 68 | "singleQuote": true, 69 | "trailingComma": "none" 70 | }, 71 | "scripts": { 72 | "lint": "eslint .", 73 | "preunit": "bin/sync-vendor-libs.sh", 74 | "unit": "docker-compose run --rm mocha", 75 | "test": "npm run lint && npm run unit", 76 | "posttest": "docker-compose down -v", 77 | "build": "cd js && uglifyjs load-image.js load-image-scale.js load-image-meta.js load-image-fetch.js load-image-orientation.js load-image-exif.js load-image-exif-map.js load-image-iptc.js load-image-iptc-map.js --ie8 -c -m -o load-image.all.min.js --source-map url=load-image.all.min.js.map", 78 | "preversion": "npm test", 79 | "version": "npm run build && git add -A js", 80 | "postversion": "git push --tags origin master master:gh-pages && npm publish" 81 | }, 82 | "files": [ 83 | "js/*.js", 84 | "js/*.js.map" 85 | ], 86 | "main": "js/index.js" 87 | } 88 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | 19 | 20 | JavaScript Load Image Test 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /test/vendor/mocha.css: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | body { 4 | margin:0; 5 | } 6 | 7 | #mocha { 8 | font: 20px/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif; 9 | margin: 60px 50px; 10 | } 11 | 12 | #mocha ul, 13 | #mocha li { 14 | margin: 0; 15 | padding: 0; 16 | } 17 | 18 | #mocha ul { 19 | list-style: none; 20 | } 21 | 22 | #mocha h1, 23 | #mocha h2 { 24 | margin: 0; 25 | } 26 | 27 | #mocha h1 { 28 | margin-top: 15px; 29 | font-size: 1em; 30 | font-weight: 200; 31 | } 32 | 33 | #mocha h1 a { 34 | text-decoration: none; 35 | color: inherit; 36 | } 37 | 38 | #mocha h1 a:hover { 39 | text-decoration: underline; 40 | } 41 | 42 | #mocha .suite .suite h1 { 43 | margin-top: 0; 44 | font-size: .8em; 45 | } 46 | 47 | #mocha .hidden { 48 | display: none; 49 | } 50 | 51 | #mocha h2 { 52 | font-size: 12px; 53 | font-weight: normal; 54 | cursor: pointer; 55 | } 56 | 57 | #mocha .suite { 58 | margin-left: 15px; 59 | } 60 | 61 | #mocha .test { 62 | margin-left: 15px; 63 | overflow: hidden; 64 | } 65 | 66 | #mocha .test.pending:hover h2::after { 67 | content: '(pending)'; 68 | font-family: arial, sans-serif; 69 | } 70 | 71 | #mocha .test.pass.medium .duration { 72 | background: #c09853; 73 | } 74 | 75 | #mocha .test.pass.slow .duration { 76 | background: #b94a48; 77 | } 78 | 79 | #mocha .test.pass::before { 80 | content: '✓'; 81 | font-size: 12px; 82 | display: block; 83 | float: left; 84 | margin-right: 5px; 85 | color: #00d6b2; 86 | } 87 | 88 | #mocha .test.pass .duration { 89 | font-size: 9px; 90 | margin-left: 5px; 91 | padding: 2px 5px; 92 | color: #fff; 93 | -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.2); 94 | -moz-box-shadow: inset 0 1px 1px rgba(0,0,0,.2); 95 | box-shadow: inset 0 1px 1px rgba(0,0,0,.2); 96 | -webkit-border-radius: 5px; 97 | -moz-border-radius: 5px; 98 | -ms-border-radius: 5px; 99 | -o-border-radius: 5px; 100 | border-radius: 5px; 101 | } 102 | 103 | #mocha .test.pass.fast .duration { 104 | display: none; 105 | } 106 | 107 | #mocha .test.pending { 108 | color: #0b97c4; 109 | } 110 | 111 | #mocha .test.pending::before { 112 | content: '◦'; 113 | color: #0b97c4; 114 | } 115 | 116 | #mocha .test.fail { 117 | color: #c00; 118 | } 119 | 120 | #mocha .test.fail pre { 121 | color: black; 122 | } 123 | 124 | #mocha .test.fail::before { 125 | content: '✖'; 126 | font-size: 12px; 127 | display: block; 128 | float: left; 129 | margin-right: 5px; 130 | color: #c00; 131 | } 132 | 133 | #mocha .test pre.error { 134 | color: #c00; 135 | max-height: 300px; 136 | overflow: auto; 137 | } 138 | 139 | #mocha .test .html-error { 140 | overflow: auto; 141 | color: black; 142 | display: block; 143 | float: left; 144 | clear: left; 145 | font: 12px/1.5 monaco, monospace; 146 | margin: 5px; 147 | padding: 15px; 148 | border: 1px solid #eee; 149 | max-width: 85%; /*(1)*/ 150 | max-width: -webkit-calc(100% - 42px); 151 | max-width: -moz-calc(100% - 42px); 152 | max-width: calc(100% - 42px); /*(2)*/ 153 | max-height: 300px; 154 | word-wrap: break-word; 155 | border-bottom-color: #ddd; 156 | -webkit-box-shadow: 0 1px 3px #eee; 157 | -moz-box-shadow: 0 1px 3px #eee; 158 | box-shadow: 0 1px 3px #eee; 159 | -webkit-border-radius: 3px; 160 | -moz-border-radius: 3px; 161 | border-radius: 3px; 162 | } 163 | 164 | #mocha .test .html-error pre.error { 165 | border: none; 166 | -webkit-border-radius: 0; 167 | -moz-border-radius: 0; 168 | border-radius: 0; 169 | -webkit-box-shadow: 0; 170 | -moz-box-shadow: 0; 171 | box-shadow: 0; 172 | padding: 0; 173 | margin: 0; 174 | margin-top: 18px; 175 | max-height: none; 176 | } 177 | 178 | /** 179 | * (1): approximate for browsers not supporting calc 180 | * (2): 42 = 2*15 + 2*10 + 2*1 (padding + margin + border) 181 | * ^^ seriously 182 | */ 183 | #mocha .test pre { 184 | display: block; 185 | float: left; 186 | clear: left; 187 | font: 12px/1.5 monaco, monospace; 188 | margin: 5px; 189 | padding: 15px; 190 | border: 1px solid #eee; 191 | max-width: 85%; /*(1)*/ 192 | max-width: -webkit-calc(100% - 42px); 193 | max-width: -moz-calc(100% - 42px); 194 | max-width: calc(100% - 42px); /*(2)*/ 195 | word-wrap: break-word; 196 | border-bottom-color: #ddd; 197 | -webkit-box-shadow: 0 1px 3px #eee; 198 | -moz-box-shadow: 0 1px 3px #eee; 199 | box-shadow: 0 1px 3px #eee; 200 | -webkit-border-radius: 3px; 201 | -moz-border-radius: 3px; 202 | border-radius: 3px; 203 | } 204 | 205 | #mocha .test h2 { 206 | position: relative; 207 | } 208 | 209 | #mocha .test a.replay { 210 | position: absolute; 211 | top: 3px; 212 | right: 0; 213 | text-decoration: none; 214 | vertical-align: middle; 215 | display: block; 216 | width: 15px; 217 | height: 15px; 218 | line-height: 15px; 219 | text-align: center; 220 | background: #eee; 221 | font-size: 15px; 222 | -webkit-border-radius: 15px; 223 | -moz-border-radius: 15px; 224 | border-radius: 15px; 225 | -webkit-transition:opacity 200ms; 226 | -moz-transition:opacity 200ms; 227 | -o-transition:opacity 200ms; 228 | transition: opacity 200ms; 229 | opacity: 0.3; 230 | color: #888; 231 | } 232 | 233 | #mocha .test:hover a.replay { 234 | opacity: 1; 235 | } 236 | 237 | #mocha-report.pass .test.fail { 238 | display: none; 239 | } 240 | 241 | #mocha-report.fail .test.pass { 242 | display: none; 243 | } 244 | 245 | #mocha-report.pending .test.pass, 246 | #mocha-report.pending .test.fail { 247 | display: none; 248 | } 249 | #mocha-report.pending .test.pass.pending { 250 | display: block; 251 | } 252 | 253 | #mocha-error { 254 | color: #c00; 255 | font-size: 1.5em; 256 | font-weight: 100; 257 | letter-spacing: 1px; 258 | } 259 | 260 | #mocha-stats { 261 | position: fixed; 262 | top: 15px; 263 | right: 10px; 264 | font-size: 12px; 265 | margin: 0; 266 | color: #888; 267 | z-index: 1; 268 | } 269 | 270 | #mocha-stats .progress { 271 | float: right; 272 | padding-top: 0; 273 | 274 | /** 275 | * Set safe initial values, so mochas .progress does not inherit these 276 | * properties from Bootstrap .progress (which causes .progress height to 277 | * equal line height set in Bootstrap). 278 | */ 279 | height: auto; 280 | -webkit-box-shadow: none; 281 | -moz-box-shadow: none; 282 | box-shadow: none; 283 | background-color: initial; 284 | } 285 | 286 | #mocha-stats em { 287 | color: black; 288 | } 289 | 290 | #mocha-stats a { 291 | text-decoration: none; 292 | color: inherit; 293 | } 294 | 295 | #mocha-stats a:hover { 296 | border-bottom: 1px solid #eee; 297 | } 298 | 299 | #mocha-stats li { 300 | display: inline-block; 301 | margin: 0 5px; 302 | list-style: none; 303 | padding-top: 11px; 304 | } 305 | 306 | #mocha-stats canvas { 307 | width: 40px; 308 | height: 40px; 309 | } 310 | 311 | #mocha code .comment { color: #ddd; } 312 | #mocha code .init { color: #2f6fad; } 313 | #mocha code .string { color: #5890ad; } 314 | #mocha code .keyword { color: #8a6343; } 315 | #mocha code .number { color: #2f6fad; } 316 | 317 | @media screen and (max-device-width: 480px) { 318 | #mocha { 319 | margin: 60px 0px; 320 | } 321 | 322 | #mocha #stats { 323 | position: absolute; 324 | } 325 | } 326 | --------------------------------------------------------------------------------