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.

-

Select an image file

+

File input

+

-

Or enter an image URL into the following field:

+

Or drag & drop an image file onto this webpage.

+

Options

+

+ + +

+

+ + +

Result

-
-

- 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. -

-
- -