diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 4cb7acc..7323d4f 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -5,25 +5,21 @@ on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ node-version: [14, 16]
steps:
- - uses: actions/checkout@v1
- - name: Setup Node.js
- uses: actions/setup-node@v1
+ - uses: actions/checkout@v2
+ - uses: actions/setup-node@v2
with:
- node-version: 12.x
- - name: npm install
- run: npm install
- env:
- CI: true
- - name: lint
- run: npm run lint
- env:
- CI: true
+ node-version: ${{ matrix.node-version }}
+ - run: npm install
+ - run: npm run lint
unit:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v1
+ - uses: actions/checkout@v2
- name: mocha
run: docker-compose run --rm mocha
- name: docker-compose logs
diff --git a/README.md b/README.md
index cc067d4..5759a12 100644
--- a/README.md
+++ b/README.md
@@ -4,14 +4,21 @@
## Contents
-- [Demo](#demo)
+- [Demo](https://blueimp.github.io/JavaScript-Load-Image/)
- [Description](#description)
- [Setup](#setup)
- [Usage](#usage)
- [Image loading](#image-loading)
- [Image scaling](#image-scaling)
- [Requirements](#requirements)
+- [Browser support](#browser-support)
- [API](#api)
+ - [Callback](#callback)
+ - [Function signature](#function-signature)
+ - [Cancel image loading](#cancel-image-loading)
+ - [Callback arguments](#callback-arguments)
+ - [Error handling](#error-handling)
+ - [Promise](#promise)
- [Options](#options)
- [maxWidth](#maxwidth)
- [maxHeight](#maxheight)
@@ -36,29 +43,45 @@
- [canvas](#canvas)
- [crossOrigin](#crossorigin)
- [noRevoke](#norevoke)
-- [Meta data parsing](#meta-data-parsing)
+- [Metadata parsing](#metadata-parsing)
- [Image head](#image-head)
- [Exif parser](#exif-parser)
+ - [Exif Thumbnail](#exif-thumbnail)
+ - [Exif IFD](#exif-ifd)
+ - [GPSInfo IFD](#gpsinfo-ifd)
+ - [Interoperability IFD](#interoperability-ifd)
+ - [Exif parser options](#exif-parser-options)
- [Exif writer](#exif-writer)
- [IPTC parser](#iptc-parser)
+ - [IPTC parser options](#iptc-parser-options)
- [License](#license)
- [Credits](#credits)
-## Demo
-
-[JavaScript Load Image Demo](https://blueimp.github.io/JavaScript-Load-Image/)
-
## Description
-JavaScript Load Image is a library to load images provided as File or Blob
-objects or via URL. It returns an optionally scaled and/or cropped HTML img or
-canvas element. It also provides methods to parse image meta data to extract
-IPTC and Exif tags as well as embedded thumbnail images and to restore the
-complete image header after resizing.
+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](https://iptc.org/standards/photo-metadata/) and
+[Exif](https://en.wikipedia.org/wiki/Exif) tags as well as embedded thumbnail
+images, to overwrite the Exif Orientation value and to restore the complete
+image header after resizing.
## Setup
-Include the (combined and minified) JavaScript Load Image script in your HTML
+Install via [NPM](https://www.npmjs.com/package/blueimp-load-image):
+
+```sh
+npm install blueimp-load-image
+```
+
+This will install the JavaScript files inside
+`./node_modules/blueimp-load-image/js/` relative to your current directory, from
+where you can copy them into a folder that is served by your web server.
+
+Next include the combined and minified JavaScript Load Image script in your HTML
markup:
```html
@@ -68,17 +91,31 @@ markup:
Or alternatively, choose which components you want to include:
```html
+
+
+
+
+
+
+
+
+
+
+
+
+
+
```
@@ -86,12 +123,13 @@ Or alternatively, choose which components you want to include:
### Image loading
-In your application code, use the `loadImage()` function like this:
+In your application code, use the `loadImage()` function with
+[callback](#callback) style:
```js
-document.getElementById('file-input').onchange = function (e) {
+document.getElementById('file-input').onchange = function () {
loadImage(
- e.target.files[0],
+ this.files[0],
function (img) {
document.body.appendChild(img)
},
@@ -100,10 +138,33 @@ document.getElementById('file-input').onchange = function (e) {
}
```
+Or use the [Promise](#promise) based API like this ([requires](#requirements) a
+polyfill for older browsers):
+
+```js
+document.getElementById('file-input').onchange = function () {
+ loadImage(this.files[0], { maxWidth: 600 }).then(function (data) {
+ document.body.appendChild(data.image)
+ })
+}
+```
+
+With
+[async/await](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Async_await)
+(requires a modern browser or a code transpiler like
+[Babel](https://babeljs.io/) or [TypeScript](https://www.typescriptlang.org/)):
+
+```js
+document.getElementById('file-input').onchange = async function () {
+ let data = await loadImage(this.files[0], { maxWidth: 600 })
+ document.body.appendChild(data.image)
+}
+```
+
### Image scaling
-It is also possible to use the image scaling functionality with an existing
-image:
+It is also possible to use the image scaling functionality directly with an
+existing image:
```js
var scaledImage = loadImage.scale(
@@ -114,92 +175,250 @@ var scaledImage = loadImage.scale(
## Requirements
-The JavaScript Load Image library has zero dependencies.
-
-However, JavaScript Load Image is a very suitable complement to the
-[Canvas to Blob](https://github.com/blueimp/JavaScript-Canvas-to-Blob) library.
+The JavaScript Load Image library has zero dependencies, but benefits from the
+following two
+[polyfills](https://developer.mozilla.org/en-US/docs/Glossary/Polyfill):
+
+- [blueimp-canvas-to-blob](https://github.com/blueimp/JavaScript-Canvas-to-Blob)
+ for browsers without native
+ [HTMLCanvasElement.toBlob](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob)
+ support, to create `Blob` objects out of `canvas` elements.
+- [promise-polyfill](https://github.com/taylorhakes/promise-polyfill) to be able
+ to use the
+ [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)
+ based `loadImage` API in Browsers without native `Promise` support.
+
+## Browser support
+
+Browsers which implement the following APIs support all options:
+
+- Loading images from File and Blob objects:
+ - [URL.createObjectURL](https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL)
+ or
+ [FileReader.readAsDataURL](https://developer.mozilla.org/en-US/docs/Web/API/FileReader/readAsDataURL)
+- Parsing meta data:
+ - [FileReader.readAsArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/API/FileReader/readAsArrayBuffer)
+ - [Blob.slice](https://developer.mozilla.org/en-US/docs/Web/API/Blob/slice)
+ - [DataView](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView)
+ (no [BigInt](https://developer.mozilla.org/en-US/docs/Glossary/BigInt)
+ support required)
+- Parsing meta data from images loaded via URL:
+ - [fetch Response.blob](https://developer.mozilla.org/en-US/docs/Web/API/Body/blob)
+ or
+ [XMLHttpRequest.responseType blob](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseType#blob)
+- Promise based API:
+ - [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)
+
+This includes (but is not limited to) the following browsers:
+
+- Chrome 32+
+- Firefox 29+
+- Safari 8+
+- Mobile Chrome 42+ (Android)
+- Mobile Firefox 50+ (Android)
+- Mobile Safari 8+ (iOS)
+- Edge 74+
+- Edge Legacy 12+
+- Internet Explorer 10+ `*`
+
+`*` Internet Explorer [requires](#requirements) a polyfill for the `Promise`
+based API.
+
+Loading an image from a URL and applying transformations (scaling, cropping and
+rotating - except `orientation:true`, which requires reading meta data) is
+supported by all browsers which implement the
+[HTMLCanvasElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement)
+interface.
+
+Loading an image from a URL and scaling it in size is supported by all browsers
+which implement the
+[img](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img) element and
+has been tested successfully with browser engines as old as Internet Explorer 5
+(via
+[IE11's emulation mode]()).
+
+The `loadImage()` function applies options using
+[progressive enhancement](https://en.wikipedia.org/wiki/Progressive_enhancement)
+and falls back to a configuration that is supported by the browser, e.g. if the
+`canvas` element is not supported, an equivalent `img` element is returned.
## API
+### Callback
+
+#### Function signature
+
The `loadImage()` function accepts a
-[File](https://developer.mozilla.org/en/DOM/File) or
-[Blob](https://developer.mozilla.org/en/DOM/Blob) object or a simple image URL
-(e.g. `'https://example.org/image.png'`) as first argument.
-
-If a [File](https://developer.mozilla.org/en/DOM/File) or
-[Blob](https://developer.mozilla.org/en/DOM/Blob) is passed as parameter, it
-returns a HTML `img` element if the browser supports the
-[URL](https://developer.mozilla.org/en/DOM/window.URL) API or a
-[FileReader](https://developer.mozilla.org/en/DOM/FileReader) object if
-supported, or `false`.
-It always returns a HTML
-[img](https://developer.mozilla.org/en/docs/HTML/Element/Img) element when
-passing an image URL:
+[File](https://developer.mozilla.org/en-US/docs/Web/API/File) or
+[Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob) object or an image
+URL as first argument.
+
+If a [File](https://developer.mozilla.org/en-US/docs/Web/API/File) or
+[Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob) is passed as
+parameter, it returns an HTML `img` element if the browser supports the
+[URL](https://developer.mozilla.org/en-US/docs/Web/API/URL) API, alternatively a
+[FileReader](https://developer.mozilla.org/en-US/docs/Web/API/FileReader) object
+if the `FileReader` API is supported, or `false`.
+
+It always returns an HTML
+[img](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Img) element
+when passing an image URL:
```js
-document.getElementById('file-input').onchange = function (e) {
- var loadingImage = loadImage(
- e.target.files[0],
- function (img) {
- document.body.appendChild(img)
- },
- { maxWidth: 600 }
- )
- if (!loadingImage) {
- // Alternative code ...
- }
-}
+var loadingImage = loadImage(
+ 'https://example.org/image.png',
+ function (img) {
+ document.body.appendChild(img)
+ },
+ { maxWidth: 600 }
+)
```
-The `img` element or
-[FileReader](https://developer.mozilla.org/en/DOM/FileReader) object returned by
-the `loadImage()` function allows to abort the loading process by setting the
-`onload` and `onerror` event handlers to null:
+#### Cancel image loading
+
+Some browsers (e.g. Chrome) will cancel the image loading process if the `src`
+property of an `img` element is changed.
+To avoid unnecessary requests, we can use the
+[data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs)
+of a 1x1 pixel transparent GIF image as `src` target to cancel the original
+image download.
+
+To disable callback handling, we can also unset the image event handlers and for
+maximum browser compatibility, cancel the file reading process if the returned
+object is a
+[FileReader](https://developer.mozilla.org/en-US/docs/Web/API/FileReader)
+instance:
```js
-document.getElementById('file-input').onchange = function (e) {
- var loadingImage = loadImage(
- e.target.files[0],
- function (img) {
- document.body.appendChild(img)
- },
- { maxWidth: 600 }
- )
+var loadingImage = loadImage(
+ 'https://example.org/image.png',
+ function (img) {
+ document.body.appendChild(img)
+ },
+ { maxWidth: 600 }
+)
+
+if (loadingImage) {
+ // Unset event handling for the loading image:
loadingImage.onload = loadingImage.onerror = null
+
+ // Cancel image loading process:
+ if (loadingImage.abort) {
+ // FileReader instance, stop the file reading process:
+ loadingImage.abort()
+ } else {
+ // HTMLImageElement element, cancel the original image request by changing
+ // the target source to the data URL of a 1x1 pixel transparent image GIF:
+ loadingImage.src =
+ 'data:image/gif;base64,' +
+ 'R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
+ }
}
```
-The second argument must be a `callback` function, which is called when the
-image has been loaded or an error occurred while loading the image. The callback
-function is passed two arguments.
-The first is either an HTML `img` element, a
-[canvas](https://developer.mozilla.org/en/HTML/Canvas) element, or an
-[Event](https://developer.mozilla.org/en/DOM/event) object of type `error`.
-The second is on object with the original image dimensions as properties and
-potentially additional [meta data](#meta-data-parsing):
+**Please note:**
+The `img` element (or `FileReader` instance) for the loading image is only
+returned when using the callback style API and not available with the
+[Promise](#promise) based API.
+
+#### Callback arguments
+
+For the callback style API, the second argument to `loadImage()` must be a
+`callback` function, which is called when the image has been loaded or an error
+occurred while loading the image.
+
+The callback function is passed two arguments:
+
+1. An HTML [img](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img)
+ element or
+ [canvas](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API)
+ element, or an
+ [Event](https://developer.mozilla.org/en-US/docs/Web/API/Event) object of
+ type `error`.
+2. An object with the original image dimensions as properties and potentially
+ additional [metadata](#metadata-parsing).
+
+```js
+loadImage(
+ fileOrBlobOrUrl,
+ function (img, data) {
+ document.body.appendChild(img)
+ console.log('Original image width: ', data.originalWidth)
+ console.log('Original image height: ', data.originalHeight)
+ },
+ { maxWidth: 600, meta: true }
+)
+```
+
+**Please note:**
+The original image dimensions reflect the natural width and height of the loaded
+image before applying any transformation.
+For consistent values across browsers, [metadata](#metadata-parsing) parsing has
+to be enabled via `meta:true`, so `loadImage` can detect automatic image
+orientation and normalize the dimensions.
+
+#### Error handling
+
+Example code implementing error handling:
```js
-var imageUrl = 'https://example.org/image.png'
loadImage(
- imageUrl,
+ fileOrBlobOrUrl,
function (img, data) {
if (img.type === 'error') {
- console.error('Error loading image ' + imageUrl)
+ console.error('Error loading image file')
} else {
document.body.appendChild(img)
- console.log('Original image width: ', data.originalWidth)
- console.log('Original image height: ', data.originalHeight)
}
},
{ maxWidth: 600 }
)
```
+### Promise
+
+If the `loadImage()` function is called without a `callback` function as second
+argument and the
+[Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)
+API is available, it returns a `Promise` object:
+
+```js
+loadImage(fileOrBlobOrUrl, { maxWidth: 600, meta: true })
+ .then(function (data) {
+ document.body.appendChild(data.image)
+ console.log('Original image width: ', data.originalWidth)
+ console.log('Original image height: ', data.originalHeight)
+ })
+ .catch(function (err) {
+ // Handling image loading errors
+ console.log(err)
+ })
+```
+
+The `Promise` resolves with an object with the following properties:
+
+- `image`: An HTML
+ [img](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img) or
+ [canvas](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API) element.
+- `originalWidth`: The original width of the image.
+- `originalHeight`: The original height of the image.
+
+Please also read the note about original image dimensions normalization in the
+[callback arguments](#callback-arguments) section.
+
+If [metadata](#metadata-parsing) has been parsed, additional properties might be
+present on the object.
+
+If image loading fails, the `Promise` rejects with an
+[Event](https://developer.mozilla.org/en-US/docs/Web/API/Event) object of type
+`error`.
+
## Options
-The optional third argument to `loadImage()` is a map of options.
+The optional options argument to `loadImage()` allows to configure the image
+loading.
-They can be used the following way:
+It can be used the following way with the callback style:
```js
loadImage(
@@ -217,147 +436,209 @@ loadImage(
)
```
+Or the following way with the `Promise` based API:
+
+```js
+loadImage(fileOrBlobOrUrl, {
+ maxWidth: 600,
+ maxHeight: 300,
+ minWidth: 100,
+ minHeight: 50,
+ canvas: true
+}).then(function (data) {
+ document.body.appendChild(data.image)
+})
+```
+
All settings are optional. By default, the image is returned as HTML `img`
element without any image size restrictions.
### maxWidth
-Defines the maximum width of the img/canvas element.
+Defines the maximum width of the `img`/`canvas` element.
### maxHeight
-Defines the maximum height of the img/canvas element.
+Defines the maximum height of the `img`/`canvas` element.
### minWidth
-Defines the minimum width of the img/canvas element.
+Defines the minimum width of the `img`/`canvas` element.
### minHeight
-Defines the minimum height of the img/canvas element.
+Defines the minimum height of the `img`/`canvas` element.
### sourceWidth
The width of the sub-rectangle of the source image to draw into the destination
canvas.
- Defaults to the source image width and requires `canvas: true`.
+Defaults to the source image width and requires `canvas: true`.
### sourceHeight
The height of the sub-rectangle of the source image to draw into the destination
canvas.
- Defaults to the source image height and requires `canvas: true`.
+Defaults to the source image height and requires `canvas: true`.
### top
The top margin of the sub-rectangle of the source image.
- Defaults to `0` and requires `canvas: true`.
+Defaults to `0` and requires `canvas: true`.
### right
The right margin of the sub-rectangle of the source image.
- Defaults to `0` and requires `canvas: true`.
+Defaults to `0` and requires `canvas: true`.
### bottom
The bottom margin of the sub-rectangle of the source image.
- Defaults to `0` and requires `canvas: true`.
+Defaults to `0` and requires `canvas: true`.
### left
The left margin of the sub-rectangle of the source image.
- Defaults to `0` and requires `canvas: true`.
+Defaults to `0` and requires `canvas: true`.
### contain
Scales the image up/down to contain it in the max dimensions if set to `true`.
- This emulates the CSS feature [background-image: contain](https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Scaling_background_images#contain).
+This emulates the CSS feature
+[background-image: contain](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Backgrounds_and_Borders/Resizing_background_images#contain).
### cover
Scales the image up/down to cover the max dimensions with the image dimensions
if set to `true`.
- This emulates the CSS feature [background-image: cover](https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Scaling_background_images#cover).
+This emulates the CSS feature
+[background-image: cover](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Backgrounds_and_Borders/Resizing_background_images#cover).
### aspectRatio
Crops the image to the given aspect ratio (e.g. `16/9`).
- Setting the `aspectRatio` also enables the `crop` option.
+Setting the `aspectRatio` also enables the `crop` option.
### pixelRatio
Defines the ratio of the canvas pixels to the physical image pixels on the
screen.
- Should be set to `window.devicePixelRatio` unless the scaled image is not rendered
-on screen.
- Defaults to `1` and requires `canvas: true`.
+Should be set to
+[window.devicePixelRatio](https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio)
+unless the scaled image is not rendered on screen.
+Defaults to `1` and requires `canvas: true`.
### downsamplingRatio
-Defines the ratio in which the image is downsampled.
- By default, images are downsampled in one step. With a ratio of `0.5`, each step
-scales the image to half the size, before reaching the target dimensions.
- Requires `canvas: true`.
+Defines the ratio in which the image is downsampled (scaled down in steps).
+By default, images are downsampled in one step.
+With a ratio of `0.5`, each step scales the image to half the size, before
+reaching the target dimensions.
+Requires `canvas: true`.
### imageSmoothingEnabled
If set to `false`,
[disables image smoothing](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/imageSmoothingEnabled).
- Defaults to `true` and requires `canvas: true`.
+Defaults to `true` and requires `canvas: true`.
### imageSmoothingQuality
Sets the
[quality of image smoothing](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/imageSmoothingQuality).
- Possible values: `'low'`, `'medium'`, `'high'`
- Defaults to `'low'` and requires `canvas: true`.
+Possible values: `'low'`, `'medium'`, `'high'`
+Defaults to `'low'` and requires `canvas: true`.
### crop
-Crops the image to the maxWidth/maxHeight constraints if set to `true`.
- Enabling the `crop` option also enables the `canvas` option.
+Crops the image to the `maxWidth`/`maxHeight` constraints if set to `true`.
+Enabling the `crop` option also enables the `canvas` option.
### orientation
Transform the canvas according to the specified Exif orientation, which can be
-an `integer` in the range of `1` to `8` or the boolean value `true`.
- When set to `true`, it will set the orientation value based on the EXIF data of
-the image, which will be parsed automatically if the exif library is available.
+an `integer` in the range of `1` to `8` or the boolean value `true`.
+
+When set to `true`, it will set the orientation value based on the Exif data of
+the image, which will be parsed automatically if the Exif extension is
+available.
+
+Exif orientation values to correctly display the letter F:
+
+```
+ 1 2
+ ██████ ██████
+ ██ ██
+ ████ ████
+ ██ ██
+ ██ ██
+
+ 3 4
+ ██ ██
+ ██ ██
+ ████ ████
+ ██ ██
+ ██████ ██████
+
+ 5 6
+██████████ ██
+██ ██ ██ ██
+██ ██████████
+
+ 7 8
+ ██ ██████████
+ ██ ██ ██ ██
+██████████ ██
+```
+
+Setting `orientation` to `true` enables the `canvas` and `meta` options, unless
+the browser supports automatic image orientation (see
+[browser support for image-orientation](https://caniuse.com/#feat=css-image-orientation)).
+
+Setting `orientation` to `1` enables the `canvas` and `meta` options if the
+browser does support automatic image orientation (to allow reset of the
+orientation).
-Setting `orientation` to an integer in the range of `2` to `8` enables the
-`canvas` option.
- Setting `orientation` to `true` enables the `canvas` and `meta` options, unless
-the browser supports automatic image orientation (see [browser support for image-orientation](https://caniuse.com/#feat=css-image-orientation)).
+Setting `orientation` to an integer in the range of `2` to `8` always enables
+the `canvas` option and also enables the `meta` option if the browser supports
+automatic image orientation (again to allow reset).
### meta
-Automatically parses the image meta data if set to `true`.
- The meta data is passed to the callback as part of the second argument.
- If the file is given as URL and the browser supports the
-[fetch API](https://developer.mozilla.org/en/docs/Web/API/Fetch_API), fetches
-the file as Blob to be able to parse the meta data.
+Automatically parses the image metadata if set to `true`.
+
+If metadata has been found, the data object passed as second argument to the
+callback function has additional properties (see
+[metadata parsing](#metadata-parsing)).
+
+If the file is given as URL and the browser supports the
+[fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) or the
+XHR
+[responseType](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseType)
+`blob`, fetches the file as `Blob` to be able to parse the metadata.
### canvas
-Returns the image as [canvas](https://developer.mozilla.org/en/HTML/Canvas)
-element if set to `true`.
+Returns the image as
+[canvas](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API) element if
+set to `true`.
### crossOrigin
-Sets the crossOrigin property on the img element for loading
-[CORS enabled images](https://developer.mozilla.org/en-US/docs/HTML/CORS_Enabled_Image).
+Sets the `crossOrigin` property on the `img` element for loading
+[CORS enabled images](https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image).
### noRevoke
By default, the
-[created object URL](https://developer.mozilla.org/en/DOM/window.URL.createObjectURL)
+[created object URL](https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL)
is revoked after the image has been loaded, except when this option is set to
`true`.
-## Meta data parsing
+## Metadata parsing
-If the Load Image Meta extension is included, it is also possible to parse image
-meta data automatically with the `meta` option:
+If the Load Image Meta extension is included, it is possible to parse image meta
+data automatically with the `meta` option:
```js
loadImage(
@@ -388,29 +669,46 @@ loadImage.parseMetaData(
)
```
-The Meta data extension also adds additional options used for the
-`parseMetaData` method:
+Or using the
+[Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)
+based API:
+
+```js
+loadImage
+ .parseMetaData(fileOrBlob, {
+ maxMetaDataSize: 262144
+ })
+ .then(function (data) {
+ console.log('Original image head: ', data.imageHead)
+ console.log('Exif data: ', data.exif) // requires exif extension
+ console.log('IPTC data: ', data.iptc) // requires iptc extension
+ })
+```
+
+The Metadata extension adds additional options used for the `parseMetaData`
+method:
-- `maxMetaDataSize`: Maximum number of bytes of meta data to parse.
+- `maxMetaDataSize`: Maximum number of bytes of metadata to parse.
- `disableImageHead`: Disable parsing the original image head.
-- `disableMetaDataParsers`: Disable parsing meta data (image head only)
+- `disableMetaDataParsers`: Disable parsing metadata (image head only)
### Image head
Resized JPEG images can be combined with their original image head via
`loadImage.replaceHead`, which requires the resized image as `Blob` object as
-first argument and an `ArrayBuffer` image head as second argument. The third
-argument must be a `callback` function, which is called with the new `Blob`
-object:
+first argument and an `ArrayBuffer` image head as second argument.
+
+With callback style, the third argument must be a `callback` function, which is
+called with the new `Blob` object:
```js
loadImage(
fileOrBlobOrUrl,
function (img, data) {
- if (data.imageHead && data.exif) {
+ if (data.imageHead) {
img.toBlob(function (blob) {
loadImage.replaceHead(blob, data.imageHead, function (newBlob) {
- // do something with newBlob
+ // do something with the new Blob object
})
}, 'image/jpeg')
}
@@ -419,12 +717,37 @@ loadImage(
)
```
-**Note:**
-Blob objects of resized images can be created via
-[canvas.toBlob](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob).
-For browsers which don't have native support, a
-[canvas.toBlob polyfill](https://github.com/blueimp/JavaScript-Canvas-to-Blob)
-is available.
+Or using the
+[Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)
+based API like this:
+
+```js
+loadImage(fileOrBlobOrUrl, { meta: true, canvas: true, maxWidth: 800 })
+ .then(function (data) {
+ if (!data.imageHead) throw new Error('Could not parse image metadata')
+ return new Promise(function (resolve) {
+ data.image.toBlob(function (blob) {
+ data.blob = blob
+ resolve(data)
+ }, 'image/jpeg')
+ })
+ })
+ .then(function (data) {
+ return loadImage.replaceHead(data.blob, data.imageHead)
+ })
+ .then(function (blob) {
+ // do something with the new Blob object
+ })
+ .catch(function (err) {
+ console.error(err)
+ })
+```
+
+**Please note:**
+`Blob` objects of resized images can be created via
+[HTMLCanvasElement.toBlob](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob).
+[blueimp-canvas-to-blob](https://github.com/blueimp/JavaScript-Canvas-to-Blob)
+provides a polyfill for browsers without native `canvas.toBlob()` support.
### Exif parser
@@ -454,10 +777,10 @@ var orientationOffset = data.exifOffsets.get('Orientation')
By default, only the following names are mapped:
- `Orientation`
-- `Thumbnail`
-- `Exif`
-- `GPSInfo`
-- `Interoperability`
+- `Thumbnail` (see [Exif Thumbnail](#exif-thumbnail))
+- `Exif` (see [Exif IFD](#exif-ifd))
+- `GPSInfo` (see [GPSInfo IFD](#gpsinfo-ifd))
+- `Interoperability` (see [Interoperability IFD](#interoperability-ifd))
If you also include the Load Image Exif Map library, additional tag mappings
become available, as well as three additional methods:
@@ -467,19 +790,105 @@ become available, as well as three additional methods:
- `exif.getAll()`
```js
-var flashText = data.exif.getText('Flash') // e.g.: 'Flash fired, auto mode',
+var orientationText = data.exif.getText('Orientation') // e.g. "Rotate 90° CW"
-var name = data.exif.getName(0x0112) // Orientation
+var name = data.exif.getName(0x0112) // "Orientation"
// A map of all parsed tags with their mapped names/text as keys/values:
var allTags = data.exif.getAll()
```
-The Exif parser also adds additional options for the parseMetaData method, to
-disable certain aspects of the parser:
+#### Exif Thumbnail
+
+Example code displaying a thumbnail image embedded into the Exif metadata:
+
+```js
+loadImage(
+ fileOrBlobOrUrl,
+ function (img, data) {
+ var exif = data.exif
+ var thumbnail = exif && exif.get('Thumbnail')
+ var blob = thumbnail && thumbnail.get('Blob')
+ if (blob) {
+ loadImage(
+ blob,
+ function (thumbImage) {
+ document.body.appendChild(thumbImage)
+ },
+ { orientation: exif.get('Orientation') }
+ )
+ }
+ },
+ { meta: true }
+)
+```
+
+#### Exif IFD
+
+Example code displaying data from the Exif IFD (Image File Directory) that
+contains Exif specified TIFF tags:
+
+```js
+loadImage(
+ fileOrBlobOrUrl,
+ function (img, data) {
+ var exifIFD = data.exif && data.exif.get('Exif')
+ if (exifIFD) {
+ // Map of all Exif IFD tags with their mapped names/text as keys/values:
+ console.log(exifIFD.getAll())
+ // A specific Exif IFD tag value:
+ console.log(exifIFD.get('UserComment'))
+ }
+ },
+ { meta: true }
+)
+```
+
+#### GPSInfo IFD
+
+Example code displaying data from the Exif IFD (Image File Directory) that
+contains [GPS](https://en.wikipedia.org/wiki/Global_Positioning_System) info:
+
+```js
+loadImage(
+ fileOrBlobOrUrl,
+ function (img, data) {
+ var gpsInfo = data.exif && data.exif.get('GPSInfo')
+ if (gpsInfo) {
+ // Map of all GPSInfo tags with their mapped names/text as keys/values:
+ console.log(gpsInfo.getAll())
+ // A specific GPSInfo tag value:
+ console.log(gpsInfo.get('GPSLatitude'))
+ }
+ },
+ { meta: true }
+)
+```
+
+#### Interoperability IFD
+
+Example code displaying data from the Exif IFD (Image File Directory) that
+contains Interoperability data:
+
+```js
+loadImage(
+ fileOrBlobOrUrl,
+ function (img, data) {
+ var interoperabilityData = data.exif && data.exif.get('Interoperability')
+ if (interoperabilityData) {
+ // The InteroperabilityIndex tag value:
+ console.log(interoperabilityData.get('InteroperabilityIndex'))
+ }
+ },
+ { meta: true }
+)
+```
+
+#### Exif parser options
+
+The Exif parser adds additional options:
- `disableExif`: Disables Exif parsing when `true`.
-- `disableExifThumbnail`: Disables parsing of Thumbnail data when `true`.
- `disableExifOffsets`: Disables storing Exif tag offsets when `true`.
- `includeExifTags`: A map of Exif tags to include for parsing (includes all but
the excluded tags by default).
@@ -497,8 +906,10 @@ loadImage.parseMetaData(
{
includeExifTags: {
0x0112: true, // Orientation
- 0x0201: true, // JPEGInterchangeFormat (Thumbnail data offset)
- 0x0202: true, // JPEGInterchangeFormatLength (Thumbnail data length)
+ ifd1: {
+ 0x0201: true, // JPEGInterchangeFormat (Thumbnail data offset)
+ 0x0202: true // JPEGInterchangeFormatLength (Thumbnail data length)
+ },
0x8769: {
// ExifIFDPointer
0x9000: true // ExifVersion
@@ -551,6 +962,13 @@ loadImage(
)
```
+**Please note:**
+The Exif writer relies on the Exif tag offsets being available as
+`data.exifOffsets` property, which requires that Exif data has been parsed from
+the image.
+The Exif writer can only change existing values, not add new tags, e.g. it
+cannot add an Exif `Orientation` tag for an image that does not have one.
+
### IPTC parser
If you include the Load Image IPTC Parser extension, the argument passed to the
@@ -593,8 +1011,9 @@ var name = data.iptc.getName(5) // ObjectName
var allTags = data.iptc.getAll()
```
-The IPTC parser also adds additional options for the parseMetaData method, to
-disable certain aspects of the parser:
+#### IPTC parser options
+
+The IPTC parser adds additional options:
- `disableIptc`: Disables IPTC parsing when true.
- `disableIptcOffsets`: Disables storing IPTC tag offsets when `true`.
@@ -638,13 +1057,14 @@ loadImage.parseMetaData(
## License
-The JavaScript Load Image script is released under the
+The JavaScript Load Image library is released under the
[MIT license](https://opensource.org/licenses/MIT).
## Credits
-- Image meta data handling implementation based on the help and contribution of
+- Original image metadata handling implemented with the help and contribution of
Achim Stöhr.
-- Exif tags mapping based on Jacob Seidelin's
- [exif-js](https://github.com/jseidelin/exif-js) library.
-- IPTC parser implementation by [Dave Bevan](https://github.com/bevand10).
+- Original Exif tags mapping based on Jacob Seidelin's
+ [exif-js](https://github.com/exif-js/exif-js) library.
+- Original IPTC parser implementation by
+ [Dave Bevan](https://github.com/bevand10).
diff --git a/bin/sync-vendor-libs.sh b/bin/sync-vendor-libs.sh
new file mode 100755
index 0000000..bff8eb3
--- /dev/null
+++ b/bin/sync-vendor-libs.sh
@@ -0,0 +1,8 @@
+#!/bin/sh
+cd "$(dirname "$0")/.."
+cp node_modules/blueimp-canvas-to-blob/js/canvas-to-blob.js js/vendor/
+cp node_modules/jquery/dist/jquery.js js/vendor/
+cp node_modules/promise-polyfill/dist/polyfill.js js/vendor/promise-polyfill.js
+cp node_modules/chai/chai.js test/vendor/
+cp node_modules/mocha/mocha.js test/vendor/
+cp node_modules/mocha/mocha.css test/vendor/
diff --git a/css/demo.css b/css/demo.css
index fb0f6a3..97e4ec5 100644
--- a/css/demo.css
+++ b/css/demo.css
@@ -10,63 +10,220 @@
*/
body {
- max-width: 750px;
+ max-width: 990px;
margin: 0 auto;
padding: 1em;
- font-family: 'Lucida Grande', 'Lucida Sans Unicode', Arial, sans-serif;
- font-size: 1em;
- line-height: 1.4em;
- background: #222;
- color: #fff;
+ font-family: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue',
+ Arial, sans-serif;
-webkit-text-size-adjust: 100%;
- -ms-text-size-adjust: 100%;
+ line-height: 1.4;
+ background: #212121;
+ color: #dedede;
}
a {
- color: orange;
+ color: #61afef;
text-decoration: none;
}
-img {
- border: 0;
- vertical-align: middle;
+a:visited {
+ color: #56b6c2;
}
-h1 {
- line-height: 1em;
-}
-h2,
-h3 {
- margin-top: 2em;
+a:hover {
+ color: #98c379;
}
table {
width: 100%;
word-wrap: break-word;
table-layout: fixed;
border-collapse: collapse;
- margin-bottom: 20px;
}
-tr {
- background: #fff;
- color: #222;
+figure {
+ margin: 0;
+ padding: 0.75em;
+ border-radius: 5px;
+ display: inline-block;
+}
+table,
+figure {
+ margin-bottom: 1.25em;
+}
+tr,
+figure {
+ background: #363636;
}
tr:nth-child(odd) {
- background: #eee;
- color: #222;
+ background: #414141;
+}
+td,
+th {
+ padding: 0.5em 0.75em;
+ text-align: left;
}
-td {
- padding: 10px;
+img,
+canvas {
+ max-width: 100%;
+ border: 0;
+ vertical-align: middle;
}
-#result,
-#thumbnail {
- margin-bottom: 20px;
- padding: 20px;
- background: #fff;
- color: #222;
- text-align: center;
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ margin-top: 1.5em;
+ margin-bottom: 0.5em;
}
-.jcrop-holder {
- margin: 0 auto;
+h1 {
+ margin-top: 0.5em;
+}
+label {
+ display: inline-block;
+ margin-bottom: 0.25em;
+}
+button,
+select,
+input,
+textarea {
+ -webkit-appearance: none;
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0.5em 0.75em;
+ font-family: inherit;
+ font-size: 100%;
+ line-height: 1.4;
+ background: #414141;
+ color: #dedede;
+ border: 1px solid #363636;
+ border-radius: 5px;
+ box-shadow: 0 0 4px rgba(0, 0, 0, 0.07);
+}
+input,
+textarea {
+ width: 100%;
+ box-shadow: inset 0 0 4px rgba(0, 0, 0, 0.07);
+}
+textarea {
+ display: block;
+ overflow: auto;
+}
+button {
+ background: #3c76a7;
+ background: linear-gradient(180deg, #3c76a7, #225c8d);
+ border-color: #225c8d;
+ color: #fff;
+}
+button[type='submit'] {
+ background: #6fa349;
+ background: linear-gradient(180deg, #6fa349, #568a30);
+ border-color: #568a30;
+}
+button[type='reset'] {
+ background: #d79435;
+ background: linear-gradient(180deg, #d79435, #be7b1c);
+ border-color: #be7b1c;
+}
+select {
+ display: block;
+ padding-right: 2.25em;
+ background: #3c76a7;
+ 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')
+ no-repeat right 0.75em center/0.75em,
+ linear-gradient(180deg, #3c76a7, #225c8d);
+ border-color: #225c8d;
+ color: #fff;
+}
+button:active,
+select:active {
+ box-shadow: inset 0 0 8px rgba(0, 0, 0, 0.5);
+}
+select::-ms-expand {
+ display: none;
+}
+option {
+ color: #212121;
+}
+input[type='checkbox'] {
+ -webkit-appearance: checkbox;
+ width: auto;
+ padding: initial;
+ box-shadow: none;
+}
+input[type='file'] {
+ max-width: 100%;
+ padding: 0;
+ background: none;
+ border: 0;
+ border-radius: 0;
+ box-shadow: none;
+}
+
+input[type='file']::-webkit-file-upload-button {
+ -webkit-appearance: none;
+ box-sizing: border-box;
+ padding: 0.5em 0.75em;
+ font-family: inherit;
+ font-size: 100%;
+ line-height: 1.4;
+ background: linear-gradient(180deg, #3c76a7, #225c8d);
+ border: 1px solid #225c8d;
+ color: #fff;
+ border-radius: 5px;
+ box-shadow: 0 0 4px rgba(0, 0, 0, 0.07);
+}
+input[type='file']::-webkit-file-upload-button:active {
+ box-shadow: inset 0 0 8px rgba(0, 0, 0, 0.5);
+}
+input[type='file']::-ms-browse {
+ box-sizing: border-box;
+ padding: 0.5em 0.75em;
+ font-family: inherit;
+ font-size: 100%;
+ line-height: 1.4;
+ background: linear-gradient(180deg, #3c76a7, #225c8d);
+ border: 1px solid #225c8d;
+ color: #fff;
+ border-radius: 5px;
+ box-shadow: 0 0 4px rgba(0, 0, 0, 0.07);
+}
+input[type='file']::-ms-browse:active {
+ box-shadow: inset 0 0 8px rgba(0, 0, 0, 0.5);
+}
+
+@media (prefers-color-scheme: light) {
+ body {
+ background: #ececec;
+ color: #212121;
+ }
+ a {
+ color: #225c8d;
+ }
+ a:visited {
+ color: #378f9a;
+ }
+ a:hover {
+ color: #6fa349;
+ }
+ figure,
+ tr {
+ background: #fff;
+ color: #212121;
+ }
+ tr:nth-child(odd) {
+ background: #f6f6f6;
+ }
+ input,
+ textarea {
+ background: #fff;
+ border-color: #d1d1d1;
+ color: #212121;
+ }
+}
+
+#result {
+ display: block;
}
-@media (min-width: 481px) {
+@media (min-width: 540px) {
#navigation {
list-style: none;
padding: 0;
@@ -74,7 +231,7 @@ td {
#navigation li {
display: inline-block;
}
- #navigation li:not(:first-child):before {
- content: '| ';
+ #navigation li:not(:first-child)::before {
+ content: ' | ';
}
}
diff --git a/docker-compose.yml b/docker-compose.yml
index 74f66fe..6bba835 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -4,7 +4,6 @@ services:
image: nginx:alpine
ports:
- 127.0.0.1:80:80
- - ${SERVER_HOST:-127.0.0.1}:${SERVER_PORT-}:80
volumes:
- .:/usr/share/nginx/html:ro
mocha:
diff --git a/index.html b/index.html
index bc619ec..05d5ac0 100644
--- a/index.html
+++ b/index.html
@@ -20,7 +20,7 @@
JavaScript Load Image
+
JavaScript Load Image Demo
@@ -36,24 +44,31 @@
JavaScript Load Image Demo
>JavaScript Load Image
is a library to load images provided as
- File or
- Blob objects or
- via URL.
- It returns an optionally scaled and/or
- cropped HTML
- img
+ File
+ or
+ Blob
+ objects or via URL.
+ It returns an optionally scaled,
+ cropped or rotated HTML
+ img
or
- canvas
- element.
- It also provides a method to parse image meta data to extract
+ canvas
+ element.
+
+
+ It also provides methods to parse image metadata to extract
IPTC and
Exif tags as well as
- embedded thumbnail images and to restore the complete image header after
- resizing.
+ embedded thumbnail images, to overwrite the Exif Orientation value and to
+ restore the complete image header after resizing.
- This demo works only in browsers with support for the
- URL or
- FileReader
+
+
+ Loading images from File objects requires support for the
+ URL
+ or
+ FileReader
API.
-
-
-
-
Exif meta data
-
-
-
-
IPTC meta data
+
+
+
+
Image metadata
+
-
diff --git a/js/demo/demo.js b/js/demo/demo.js
index 6dc9fa3..a95cf55 100644
--- a/js/demo/demo.js
+++ b/js/demo/demo.js
@@ -14,11 +14,17 @@
$(function () {
'use strict'
- var result = $('#result')
- var exifNode = $('#exif')
- var iptcNode = $('#iptc')
+ var resultNode = $('#result')
+ var metaNode = $('#meta')
var thumbNode = $('#thumbnail')
var actionsNode = $('#actions')
+ var orientationNode = $('#orientation')
+ var imageSmoothingNode = $('#image-smoothing')
+ var fileInputNode = $('#file-input')
+ var urlNode = $('#url')
+ var editNode = $('#edit')
+ var cropNode = $('#crop')
+ var cancelNode = $('#cancel')
var coordinates
var jcropAPI
@@ -26,17 +32,20 @@ $(function () {
* Displays tag data
*
* @param {*} node jQuery node
- * @param {object} tags Tags object
+ * @param {object} tags Tags map
+ * @param {string} title Tags title
*/
- function displayTagData(node, tags) {
- var table = $('
')
+ function displayTagData(node, tags, title) {
+ var table = $('
')
var row = $('
')
var cell = $('
')
+ var headerCell = $('
')
var prop
+ table.append(row.clone().append(headerCell.clone().text(title)))
for (prop in tags) {
if (Object.prototype.hasOwnProperty.call(tags, prop)) {
if (typeof tags[prop] === 'object') {
- displayTagData(node, tags[prop])
+ displayTagData(node, tags[prop], prop)
continue
}
table.append(
@@ -59,7 +68,7 @@ $(function () {
*/
function displayThumbnailImage(node, thumbnail, options) {
if (thumbnail) {
- var link = $('')
+ var link = $('')
.attr('href', loadImage.createObjectURL(thumbnail))
.attr('download', 'thumbnail.jpg')
.appendTo(node)
@@ -75,52 +84,72 @@ $(function () {
}
/**
- * Displays meta data
+ * Displays metadata
*
- * @param {object} [data] Meta data object
+ * @param {object} [data] Metadata object
*/
function displayMetaData(data) {
if (!data) return
+ metaNode.data(data)
var exif = data.exif
var iptc = data.iptc
if (exif) {
- displayThumbnailImage(thumbNode, exif.get('Thumbnail'), {
- orientation: exif.get('Orientation')
- })
- displayTagData(exifNode, exif.getAll())
+ var thumbnail = exif.get('Thumbnail')
+ if (thumbnail) {
+ displayThumbnailImage(thumbNode, thumbnail.get('Blob'), {
+ orientation: exif.get('Orientation')
+ })
+ }
+ displayTagData(metaNode, exif.getAll(), 'TIFF')
}
if (iptc) {
- displayTagData(iptcNode, iptc.getAll())
+ displayTagData(metaNode, iptc.getAll(), 'IPTC')
}
}
+ /**
+ * Removes meta data from the page
+ */
+ function removeMetaData() {
+ metaNode.hide().removeData().find('table').remove()
+ thumbNode.hide().empty()
+ }
+
/**
* Updates the results view
*
* @param {*} img Image or canvas element
- * @param {object} [data] Meta data object
+ * @param {object} [data] Metadata object
+ * @param {boolean} [keepMetaData] Keep meta data if true
*/
- function updateResults(img, data) {
- if (!(img.src || img instanceof HTMLCanvasElement)) {
- result.children().replaceWith($('Loading image file failed'))
- return
- }
- var content = $('').append(img)
- result.children().replaceWith(content)
- if (data) {
- if (img.getContext) {
+ function updateResults(img, data, keepMetaData) {
+ var isCanvas = window.HTMLCanvasElement && img instanceof HTMLCanvasElement
+ if (!keepMetaData) {
+ removeMetaData()
+ if (data) {
+ displayMetaData(data)
+ }
+ if (isCanvas) {
actionsNode.show()
+ } else {
+ actionsNode.hide()
}
- displayMetaData(data)
- result.data(data)
- } else {
- // eslint-disable-next-line no-param-reassign
- data = result.data()
}
- if (data.imageHead && data.exif) {
- // Reset Exif Orientation data:
- loadImage.writeExifData(data.imageHead, data, 'Orientation', 1)
+ if (!(isCanvas || img.src)) {
+ resultNode
+ .children()
+ .replaceWith($('Loading image file failed'))
+ return
+ }
+ var content = $('').append(img)
+ resultNode.children().replaceWith(content)
+ if (data.imageHead) {
+ if (data.exif) {
+ // Reset Exif Orientation data:
+ loadImage.writeExifData(data.imageHead, data, 'Orientation', 1)
+ }
img.toBlob(function (blob) {
+ if (!blob) return
loadImage.replaceHead(blob, data.imageHead, function (newBlob) {
content
.attr('href', loadImage.createObjectURL(newBlob))
@@ -137,18 +166,17 @@ $(function () {
*/
function displayImage(file) {
var options = {
- maxWidth: result.width(),
+ maxWidth: resultNode.width(),
canvas: true,
pixelRatio: window.devicePixelRatio,
downsamplingRatio: 0.5,
- orientation: true,
+ orientation: Number(orientationNode.val()) || true,
+ imageSmoothingEnabled: imageSmoothingNode.is(':checked'),
meta: true
}
- exifNode.hide().find('table').remove()
- iptcNode.hide().find('table').remove()
- thumbNode.hide().empty()
if (!loadImage(file, updateResults, options)) {
- result
+ removeMetaData()
+ resultNode
.children()
.replaceWith(
$(
@@ -184,31 +212,48 @@ $(function () {
if (url) displayImage(url)
}
- // Hide URL/FileReader API requirement message in capable browsers:
+ // Show the URL/FileReader API requirement message if not supported:
if (
window.createObjectURL ||
window.URL ||
window.webkitURL ||
window.FileReader
) {
- result.children().hide()
+ resultNode.children().hide()
+ } else {
+ resultNode.children().show()
}
$(document)
.on('dragover', function (e) {
e.preventDefault()
- var originalEvent = event.originalEvent
- if (originalEvent) originalEvent.dataTransfer.dropEffect = 'copy'
+ if (event.dataTransfer) event.dataTransfer.dropEffect = 'copy'
})
.on('drop', fileChangeHandler)
- $('#file-input').on('change', fileChangeHandler)
+ fileInputNode.on('change', fileChangeHandler)
+
+ urlNode.on('change paste input', urlChangeHandler)
- $('#url').on('change paste input', urlChangeHandler)
+ orientationNode.on('change', function () {
+ var img = resultNode.find('img, canvas')[0]
+ if (img) {
+ updateResults(
+ loadImage.scale(img, {
+ maxWidth: resultNode.width() * (window.devicePixelRatio || 1),
+ pixelRatio: window.devicePixelRatio,
+ orientation: Number(orientationNode.val()) || true,
+ imageSmoothingEnabled: imageSmoothingNode.is(':checked')
+ }),
+ metaNode.data(),
+ true
+ )
+ }
+ })
- $('#edit').on('click', function (event) {
+ editNode.on('click', function (event) {
event.preventDefault()
- var imgNode = result.find('img, canvas')
+ var imgNode = resultNode.find('img, canvas')
var img = imgNode[0]
var pixelRatio = window.devicePixelRatio || 1
var margin = img.width / pixelRatio >= 140 ? 40 : 0
@@ -239,9 +284,9 @@ $(function () {
})
})
- $('#crop').on('click', function (event) {
+ cropNode.on('click', function (event) {
event.preventDefault()
- var img = result.find('img, canvas')[0]
+ var img = resultNode.find('img, canvas')[0]
var pixelRatio = window.devicePixelRatio || 1
if (img && coordinates) {
updateResults(
@@ -250,18 +295,23 @@ $(function () {
top: coordinates.y * pixelRatio,
sourceWidth: coordinates.w * pixelRatio,
sourceHeight: coordinates.h * pixelRatio,
- minWidth: result.width(),
- maxWidth: result.width(),
+ maxWidth: resultNode.width() * pixelRatio,
+ contain: true,
pixelRatio: pixelRatio,
- downsamplingRatio: 0.5
- })
+ imageSmoothingEnabled: imageSmoothingNode.is(':checked')
+ }),
+ metaNode.data(),
+ true
)
coordinates = null
}
})
- $('#cancel').on('click', function (event) {
+ cancelNode.on('click', function (event) {
event.preventDefault()
- if (jcropAPI) jcropAPI.release()
+ if (jcropAPI) {
+ jcropAPI.release()
+ jcropAPI.disable()
+ }
})
})
diff --git a/js/load-image-exif-map.js b/js/load-image-exif-map.js
index 9684ae8..d9ce3a8 100644
--- a/js/load-image-exif-map.js
+++ b/js/load-image-exif-map.js
@@ -79,6 +79,9 @@
0xa004: 'RelatedSoundFile', // Name of related sound file
0x9003: 'DateTimeOriginal', // Date and time when the original image was generated
0x9004: 'DateTimeDigitized', // Date and time when the image was stored digitally
+ 0x9010: 'OffsetTime', // Time zone when the image file was last changed
+ 0x9011: 'OffsetTimeOriginal', // Time zone when the image was stored digitally
+ 0x9012: 'OffsetTimeDigitized', // Time zone when the image was stored digitally
0x9290: 'SubSecTime', // Fractions of seconds for DateTime
0x9291: 'SubSecTimeOriginal', // Fractions of seconds for DateTimeOriginal
0x9292: 'SubSecTimeDigitized', // Fractions of seconds for DateTimeDigitized
@@ -177,6 +180,9 @@
}
}
+ // IFD1 directory can contain any IFD0 tags:
+ ExifMapProto.tags.ifd1 = ExifMapProto.tags
+
ExifMapProto.stringValues = {
ExposureProgram: {
0: 'Undefined',
@@ -240,11 +246,15 @@
0x0045: 'Flash fired, red-eye reduction mode, return light not detected',
0x0047: 'Flash fired, red-eye reduction mode, return light detected',
0x0049: 'Flash fired, compulsory flash mode, red-eye reduction mode',
- 0x004d: 'Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected',
- 0x004f: 'Flash fired, compulsory flash mode, red-eye reduction mode, return light detected',
+ 0x004d:
+ 'Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected',
+ 0x004f:
+ 'Flash fired, compulsory flash mode, red-eye reduction mode, return light detected',
0x0059: 'Flash fired, auto mode, red-eye reduction mode',
- 0x005d: 'Flash fired, auto mode, return light not detected, red-eye reduction mode',
- 0x005f: 'Flash fired, auto mode, return light detected, red-eye reduction mode'
+ 0x005d:
+ 'Flash fired, auto mode, return light not detected, red-eye reduction mode',
+ 0x005f:
+ 'Flash fired, auto mode, return light detected, red-eye reduction mode'
},
SensingMethod: {
1: 'Undefined',
@@ -313,14 +323,14 @@
6: 'B'
},
Orientation: {
- 1: 'top-left',
- 2: 'top-right',
- 3: 'bottom-right',
- 4: 'bottom-left',
- 5: 'left-top',
- 6: 'right-top',
- 7: 'right-bottom',
- 8: 'left-bottom'
+ 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'
}
}
@@ -372,7 +382,7 @@
if (Object.prototype.hasOwnProperty.call(this, prop)) {
obj = this[prop]
if (obj && obj.getAll) {
- map[this.privateIFDs[prop].name] = obj.getAll()
+ map[this.ifds[prop].name] = obj.getAll()
} else {
name = this.tags[prop]
if (name) map[name] = this.getText(name)
@@ -384,7 +394,7 @@
ExifMapProto.getName = function (tagCode) {
var name = this.tags[tagCode]
- if (typeof name === 'object') return this.privateIFDs[tagCode].name
+ if (typeof name === 'object') return this.ifds[tagCode].name
return name
}
@@ -392,17 +402,17 @@
;(function () {
var tags = ExifMapProto.tags
var prop
- var privateIFD
+ var ifd
var subTags
// Map the tag names to tags:
for (prop in tags) {
if (Object.prototype.hasOwnProperty.call(tags, prop)) {
- privateIFD = ExifMapProto.privateIFDs[prop]
- if (privateIFD) {
+ ifd = ExifMapProto.ifds[prop]
+ if (ifd) {
subTags = tags[prop]
for (prop in subTags) {
if (Object.prototype.hasOwnProperty.call(subTags, prop)) {
- privateIFD.map[subTags[prop]] = Number(prop)
+ ifd.map[subTags[prop]] = Number(prop)
}
}
} else {
diff --git a/js/load-image-exif.js b/js/load-image-exif.js
index 7dff053..7428eef 100644
--- a/js/load-image-exif.js
+++ b/js/load-image-exif.js
@@ -32,12 +32,12 @@
*
* @name ExifMap
* @class
- * @param {number} tagCode Private IFD tag code
+ * @param {number|string} tagCode IFD tag code
*/
function ExifMap(tagCode) {
if (tagCode) {
Object.defineProperty(this, 'map', {
- value: this.privateIFDs[tagCode].map
+ value: this.ifds[tagCode].map
})
Object.defineProperty(this, 'tags', {
value: (this.tags && this.tags[tagCode]) || {}
@@ -47,13 +47,15 @@
ExifMap.prototype.map = {
Orientation: 0x0112,
- Thumbnail: 0x0201,
+ Thumbnail: 'ifd1',
+ Blob: 0x0201, // Alias for JPEGInterchangeFormat
Exif: 0x8769,
GPSInfo: 0x8825,
Interoperability: 0xa005
}
- ExifMap.prototype.privateIFDs = {
+ ExifMap.prototype.ifds = {
+ ifd1: { name: 'Thumbnail', map: ExifMap.prototype.map },
0x8769: { name: 'Exif', map: {} },
0x8825: { name: 'GPSInfo', map: {} },
0xa005: { name: 'Interoperability', map: {} }
@@ -78,13 +80,17 @@
* @returns {undefined|Blob} Returns the Thumbnail Blob or undefined
*/
function getExifThumbnail(dataView, offset, length) {
- if (!length || offset + length > dataView.byteLength) {
+ if (!length) return
+ if (offset + length > dataView.byteLength) {
console.log('Invalid Exif data: Invalid thumbnail data.')
return
}
- return new Blob([dataView.buffer.slice(offset, offset + length)], {
- type: 'image/jpeg'
- })
+ return new Blob(
+ [loadImage.bufferSlice.call(dataView.buffer, offset, offset + length)],
+ {
+ type: 'image/jpeg'
+ }
+ )
}
var ExifTagTypes = {
@@ -216,6 +222,21 @@
return values
}
+ /**
+ * Determines if the given tag should be included.
+ *
+ * @param {object} includeTags Map of tags to include
+ * @param {object} excludeTags Map of tags to exclude
+ * @param {number|string} tagCode Tag code to check
+ * @returns {boolean} True if the tag should be included
+ */
+ function shouldIncludeTag(includeTags, excludeTags, tagCode) {
+ return (
+ (!includeTags || includeTags[tagCode]) &&
+ (!excludeTags || excludeTags[tagCode] !== true)
+ )
+ }
+
/**
* Parses Exif tags.
*
@@ -253,8 +274,7 @@
for (i = 0; i < tagsNumber; i += 1) {
tagOffset = dirOffset + 2 + 12 * i
tagNumber = dataView.getUint16(tagOffset, littleEndian)
- if (includeTags && !includeTags[tagNumber]) continue
- if (excludeTags && excludeTags[tagNumber] === true) continue
+ if (!shouldIncludeTag(includeTags, excludeTags, tagNumber)) continue
tagValue = getExifValue(
dataView,
tiffOffset,
@@ -273,17 +293,17 @@
}
/**
- * Parses Private IFD tags.
+ * Parses tags in a given IFD (Image File Directory).
*
* @param {object} data Data object to store exif tags and offsets
- * @param {number} tagCode Private IFD tag code
+ * @param {number|string} tagCode IFD tag code
* @param {DataView} dataView Data view interface
* @param {number} tiffOffset TIFF offset
* @param {boolean} littleEndian Little endian encoding
* @param {object} includeTags Map of tags to include
* @param {object} excludeTags Map of tags to exclude
*/
- function parseExifPrivateIFD(
+ function parseExifIFD(
data,
tagCode,
dataView,
@@ -325,7 +345,7 @@
var tiffOffset = offset + 10
var littleEndian
var dirOffset
- var privateIFDs
+ var thumbnailIFD
// Check for the ASCII code for "Exif" (0x45786966):
if (dataView.getUint32(offset + 4) !== 0x45786966) {
// No Exif data, might be XMP data instead
@@ -366,8 +386,8 @@
data.exifTiffOffset = tiffOffset
data.exifLittleEndian = littleEndian
}
- // Parse the tags of the main image directory and retrieve the
- // offset to the next directory, usually the thumbnail directory:
+ // Parse the tags of the main image directory (IFD0) and retrieve the
+ // offset to the next directory (IFD1), usually the thumbnail directory:
dirOffset = parseExifTags(
dataView,
tiffOffset,
@@ -378,29 +398,14 @@
includeTags,
excludeTags
)
- if (dirOffset && !options.disableExifThumbnail) {
- dirOffset = parseExifTags(
- dataView,
- tiffOffset,
- tiffOffset + dirOffset,
- littleEndian,
- data.exif,
- data.exifOffsets,
- includeTags,
- excludeTags
- )
- // Check for JPEG Thumbnail offset:
- if (data.exif[0x0201] && data.exif[0x0202]) {
- data.exif[0x0201] = getExifThumbnail(
- dataView,
- tiffOffset + data.exif[0x0201],
- data.exif[0x0202] // Thumbnail data length
- )
+ if (dirOffset && shouldIncludeTag(includeTags, excludeTags, 'ifd1')) {
+ data.exif.ifd1 = dirOffset
+ if (data.exifOffsets) {
+ data.exifOffsets.ifd1 = tiffOffset + dirOffset
}
}
- privateIFDs = Object.keys(data.exif.privateIFDs)
- privateIFDs.forEach(function (tagCode) {
- parseExifPrivateIFD(
+ Object.keys(data.exif.ifds).forEach(function (tagCode) {
+ parseExifIFD(
data,
tagCode,
dataView,
@@ -410,22 +415,33 @@
excludeTags
)
})
+ thumbnailIFD = data.exif.ifd1
+ // Check for JPEG Thumbnail offset and data length:
+ if (thumbnailIFD && thumbnailIFD[0x0201]) {
+ thumbnailIFD[0x0201] = getExifThumbnail(
+ dataView,
+ tiffOffset + thumbnailIFD[0x0201],
+ thumbnailIFD[0x0202] // Thumbnail data length
+ )
+ }
}
- // Registers the Exif parser for the APP1 JPEG meta data segment:
+ // Registers the Exif parser for the APP1 JPEG metadata segment:
loadImage.metaDataParsers.jpeg[0xffe1].push(loadImage.parseExifData)
loadImage.exifWriters = {
// Orientation writer:
0x0112: function (buffer, data, value) {
- var view = new DataView(buffer, data.exifOffsets[0x0112] + 8, 2)
+ var orientationOffset = data.exifOffsets[0x0112]
+ if (!orientationOffset) return buffer
+ var view = new DataView(buffer, orientationOffset + 8, 2)
view.setUint16(0, value, data.exifLittleEndian)
return buffer
}
}
loadImage.writeExifData = function (buffer, data, id, value) {
- loadImage.exifWriters[data.exif.map[id]](buffer, data, value)
+ return loadImage.exifWriters[data.exif.map[id]](buffer, data, value)
}
loadImage.ExifMap = ExifMap
@@ -438,7 +454,6 @@
// Adds the following options to the parseMetaData method:
// - disableExif: Disables Exif parsing when true.
- // - disableExifThumbnail: Disables parsing of Thumbnail data when true.
// - disableExifOffsets: Disables storing Exif tag offsets when true.
// - includeExifTags: A map of Exif tags to include for parsing.
// - excludeExifTags: A map of Exif tags to exclude from parsing.
diff --git a/js/load-image-fetch.js b/js/load-image-fetch.js
index 7aabda1..af65797 100644
--- a/js/load-image-fetch.js
+++ b/js/load-image-fetch.js
@@ -9,7 +9,7 @@
* https://opensource.org/licenses/MIT
*/
-/* global define, module, require */
+/* global define, module, require, Promise */
;(function (factory) {
'use strict'
@@ -25,41 +25,82 @@
})(function (loadImage) {
'use strict'
- if (typeof fetch !== 'undefined' && typeof Request !== 'undefined') {
+ var global = loadImage.global
+
+ if (
+ global.fetch &&
+ global.Request &&
+ global.Response &&
+ global.Response.prototype.blob
+ ) {
loadImage.fetchBlob = function (url, callback, options) {
+ /**
+ * Fetch response handler.
+ *
+ * @param {Response} response Fetch response
+ * @returns {Blob} Fetched Blob.
+ */
+ function responseHandler(response) {
+ return response.blob()
+ }
+ if (global.Promise && typeof callback !== 'function') {
+ return fetch(new Request(url, callback)).then(responseHandler)
+ }
fetch(new Request(url, options))
- .then(function (response) {
- return response.blob()
- })
+ .then(responseHandler)
.then(callback)
- .catch(function (err) {
+ [
+ // Avoid parsing error in IE<9, where catch is a reserved word.
+ // eslint-disable-next-line dot-notation
+ 'catch'
+ ](function (err) {
callback(null, err)
})
}
} else if (
- // Check for XHR2 support:
- typeof XMLHttpRequest !== 'undefined' &&
- typeof ProgressEvent !== 'undefined'
+ global.XMLHttpRequest &&
+ // https://xhr.spec.whatwg.org/#the-responsetype-attribute
+ new XMLHttpRequest().responseType === ''
) {
loadImage.fetchBlob = function (url, callback, options) {
- // eslint-disable-next-line no-param-reassign
- options = options || {}
- var req = new XMLHttpRequest()
- req.open(options.method || 'GET', url)
- if (options.headers) {
- Object.keys(options.headers).forEach(function (key) {
- req.setRequestHeader(key, options.headers[key])
- })
- }
- req.withCredentials = options.credentials === 'include'
- req.responseType = 'blob'
- req.onload = function () {
- callback(req.response)
+ /**
+ * Promise executor
+ *
+ * @param {Function} resolve Resolution function
+ * @param {Function} reject Rejection function
+ */
+ function executor(resolve, reject) {
+ options = options || {} // eslint-disable-line no-param-reassign
+ var req = new XMLHttpRequest()
+ req.open(options.method || 'GET', url)
+ if (options.headers) {
+ Object.keys(options.headers).forEach(function (key) {
+ req.setRequestHeader(key, options.headers[key])
+ })
+ }
+ req.withCredentials = options.credentials === 'include'
+ req.responseType = 'blob'
+ req.onload = function () {
+ resolve(req.response)
+ }
+ req.onerror =
+ req.onabort =
+ req.ontimeout =
+ function (err) {
+ if (resolve === reject) {
+ // Not using Promises
+ reject(null, err)
+ } else {
+ reject(err)
+ }
+ }
+ req.send(options.body)
}
- req.onerror = req.onabort = req.ontimeout = function (err) {
- callback(null, err)
+ if (global.Promise && typeof callback !== 'function') {
+ options = callback // eslint-disable-line no-param-reassign
+ return new Promise(executor)
}
- req.send(options.body)
+ return executor(callback, callback)
}
}
})
diff --git a/js/load-image-iptc.js b/js/load-image-iptc.js
index 9f6a900..04eb796 100644
--- a/js/load-image-iptc.js
+++ b/js/load-image-iptc.js
@@ -75,7 +75,7 @@
/**
* Retrieves tag value for the given DataView and range
*
- * @param {number} tagCode Private IFD tag code
+ * @param {number} tagCode tag code
* @param {IptcMap} map IPTC tag map
* @param {DataView} dataView Data view interface
* @param {number} offset Range start
@@ -223,7 +223,7 @@
}
}
- // Registers this IPTC parser for the APP13 JPEG meta data segment:
+ // Registers this IPTC parser for the APP13 JPEG metadata segment:
loadImage.metaDataParsers.jpeg[0xffed].push(loadImage.parseIptcData)
loadImage.IptcMap = IptcMap
diff --git a/js/load-image-meta.js b/js/load-image-meta.js
index ecdb077..357b9b3 100644
--- a/js/load-image-meta.js
+++ b/js/load-image-meta.js
@@ -5,7 +5,7 @@
* Copyright 2013, Sebastian Tschan
* https://blueimp.net
*
- * Image meta data handling implementation
+ * Image metadata handling implementation
* based on the help and contribution of
* Achim Stöhr.
*
@@ -13,7 +13,7 @@
* https://opensource.org/licenses/MIT
*/
-/* global define, module, require, DataView, Uint8Array */
+/* global define, module, require, Promise, DataView, Uint8Array, ArrayBuffer */
;(function (factory) {
'use strict'
@@ -29,79 +29,99 @@
})(function (loadImage) {
'use strict'
- var hasblobSlice =
- typeof Blob !== 'undefined' &&
+ var global = loadImage.global
+ var originalTransform = loadImage.transform
+
+ var blobSlice =
+ global.Blob &&
(Blob.prototype.slice ||
Blob.prototype.webkitSlice ||
Blob.prototype.mozSlice)
- loadImage.blobSlice =
- hasblobSlice &&
- function () {
- var slice = this.slice || this.webkitSlice || this.mozSlice
- return slice.apply(this, arguments)
+ var bufferSlice =
+ (global.ArrayBuffer && ArrayBuffer.prototype.slice) ||
+ function (begin, end) {
+ // Polyfill for IE10, which does not support ArrayBuffer.slice
+ // eslint-disable-next-line no-param-reassign
+ end = end || this.byteLength - begin
+ var arr1 = new Uint8Array(this, begin, end)
+ var arr2 = new Uint8Array(end)
+ arr2.set(arr1)
+ return arr2.buffer
}
- loadImage.metaDataParsers = {
+ var metaDataParsers = {
jpeg: {
0xffe1: [], // APP1 marker
0xffed: [] // APP13 marker
}
}
- // Parses image meta data and calls the callback with an object argument
- // with the following properties:
- // * imageHead: The complete image head as ArrayBuffer (Uint8Array for IE10)
- // The options argument accepts an object and supports the following
- // properties:
- // * maxMetaDataSize: Defines the maximum number of bytes to parse.
- // * disableImageHead: Disables creating the imageHead property.
- loadImage.parseMetaData = function (file, callback, options, data) {
- // eslint-disable-next-line no-param-reassign
- options = options || {}
- // eslint-disable-next-line no-param-reassign
- data = data || {}
+ /**
+ * Parses image metadata and calls the callback with an object argument
+ * with the following property:
+ * - imageHead: The complete image head as ArrayBuffer
+ * The options argument accepts an object and supports the following
+ * properties:
+ * - maxMetaDataSize: Defines the maximum number of bytes to parse.
+ * - disableImageHead: Disables creating the imageHead property.
+ *
+ * @param {Blob} file Blob object
+ * @param {Function} [callback] Callback function
+ * @param {object} [options] Parsing options
+ * @param {object} [data] Result data object
+ * @returns {Promise