├── .gitignore ├── API.md ├── API_RU.md ├── LICENSE ├── README.md ├── README_RU.md ├── build └── apng-canvas.min.js ├── gulpfile.js ├── package-lock.json ├── package.json └── src ├── animation.js ├── crc32.js ├── loader.js ├── main.js ├── parser.js └── support-test.js /.gitignore: -------------------------------------------------------------------------------- 1 | .* 2 | node_modules 3 | test 4 | !.gitignore -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | #### APNG.ifNeeded(\[ignoreNativeAPNG boolean\]) 4 | Checks whether there is a need to use the library. 5 | 6 | **Fulfilled** (no value): The browser supports everything for the technology to work, but it does not support APNG. Usually 7 | the library should only be used in this case. 8 | 9 | If optional argument *ignoreNativeAPNG* is *true*, then native APNG support isn't tested. 10 | 11 | **Rejected** (no value): The browser has native support for APNG (if *ignoreNativeAPNG* not used) or does not support all the necessary technologies for it to work. 12 | 13 | #### APNG.animateImage(img HTMLImageElement) 14 | Creates a `canvas` element where the APNG animation plays. The `img` element is removed from the DOM and replaced by `canvas`. 15 | The `img` element attributes are preserved during replacement. 16 | 17 | **Fulfilled** (no value): The `img` element is an APNG image. 18 | 19 | **Rejected** (no value): The `img` element is not an APNG image, or there was an error when processing it. In this case the element is not replaced with `canvas`. 20 | 21 | #### APNG.releaseCanvas(canvas HTMLCanvasElement) 22 | Detaches `canvas` from animation loop. May be useful for dynamic created APNG-images. 23 | This is a synchronous method, it does not return a result. 24 | 25 | #### APNG.checkNativeFeatures() 26 | Checks which technologies are supported by the browser. 27 | 28 | **Fulfilled** (Features): Returns the *Features* object with the following fields: 29 | 30 | { 31 | TypedArrays: boolean 32 | BlobURLs: boolean 33 | requestAnimationFrame: boolean 34 | pageProtocol: boolean 35 | canvas: boolean 36 | APNG: boolean 37 | } 38 | 39 | Each field has the value *true* or *false*. *True* means the browser has built-in support for the relevant technology. 40 | The `pageProtocol` field has the value *true* if the page is loaded over the *http* or *https* protocol (the library does not work on pages downloaded 41 | over other protocols). 42 | 43 | The library can work if all fields except APNG have the value `true`. 44 | 45 | **Rejected**: N/A. 46 | 47 | #### APNG.parseBuffer(data ArrayBuffer) 48 | Parses binary data from the APNG-file. 49 | 50 | **Fulfilled** (Animation): If the transmitted data are valid APNG, then the *Animation* object is returned with the following fields: 51 | 52 | { 53 | // Properties 54 | 55 | width: int // image width 56 | height: int // image height 57 | numPlays: int // number of times to loop this animation. 0 indicates infinite looping. 58 | playTime: int // time of full animation cycle in millisecond 59 | frames: [ // animation frames 60 | { 61 | width: int // frame image width 62 | height: int // frame image height 63 | left: int // frame image horizontal offset 64 | top: int // frame image vertical offset 65 | delay: int // frame delay in millisecond 66 | disposeOp: int // frame area disposal mode (see spec.) 67 | blendOp: int // frame area blend mode (see spec.) 68 | img: HTMLImageElement // frame image 69 | } 70 | ] 71 | 72 | // Methods 73 | 74 | isPlayed(): boolean // is animation playing now? 75 | isFinished(): boolean // is animation finished (if numPlays <> 0)? 76 | play() // play animation (if not playing and not finished) 77 | rewind() // rewind animation to initial state and stop it 78 | addContext(CanvasRenderingContext2D) // play animation on this canvas context 79 | // (one animation may be played on many contexts) 80 | removeContext(CanvasRenderingContext2D) // remove context from animation 81 | } 82 | 83 | **Rejected** (string): The file is not valid APNG, or there was a parsing error. Returns a string with an error message. 84 | 85 | #### APNG.parseURL(url string) 86 | Downloads an image from the supplied URL and parses it. 87 | 88 | **Fulfilled** (Animation): If the downloaded data are valid APNG, then the *Animation* object is returned (see *APNG.parseBuffer*). 89 | The same *Animation* object is returned for the same URL. 90 | 91 | **Rejected** (mixed): There was an error when downloading or parsing. 92 | 93 | #### APNG.animateContext(url string, CanvasRenderingContext2D context) 94 | Downloads an image from the supplied URL, parses it, and plays it in the given canvas environment. 95 | 96 | **Fulfilled** (Animation): Similar to output of *APNG.parseURL*. 97 | 98 | **Rejected** (mixed): There was an error when downloading or parsing. 99 | -------------------------------------------------------------------------------- /API_RU.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | #### APNG.ifNeeded(\[ignoreNativeAPNG boolean\]) 4 | Проверяет, есть ли необходимость в использовании библиотеки. 5 | 6 | **Fulfilled** (без значения): Браузер поддерживает все необходимые для работы технологии, но не поддерживает APNG. Обычно применение 7 | библиотеки имеет смысл только в этом случае. 8 | 9 | Если необязательный параметр *ignoreNativeAPNG* имеет значение *true*, то поддержка APNG не проверяется. 10 | 11 | **Rejected** (без значения): Браузер имеет встроенную поддержку APNG (если не задан *ignoreNativeAPNG*) или поддерживает не все необходимые для работы технологии. 12 | 13 | #### APNG.animateImage(img HTMLImageElement) 14 | Создаёт элемент `canvas`, в котором проигрывается APNG-анимация. Элемент `img` удаляется из DOM-а и заменяется на `canvas`. 15 | При замене сохраняются атрибуты элемента `img`. 16 | 17 | **Fulfilled** (без значения): Переданный элемент `img` является APNG-изображением. 18 | 19 | **Rejected** (без значения): Переданный элемент `img` не является APNG-изображением, либо при его обработке произошла ошибка. Замены элемента на `canvas` при этом не происходит. 20 | 21 | #### APNG.releaseCanvas(canvas HTMLCanvasElement) 22 | Отключает заданный элемент `canvas` от цикла анимации. Это может быть полезно, если APNG-изображения на странице динамически создаются и удаляются. 23 | Это синхронный метод, он не возвращает результата. 24 | 25 | #### APNG.checkNativeFeatures() 26 | Проверяет, какие технологии поддерживает браузер. 27 | 28 | **Fulfilled** (Features): Возвращает объект *Features* со следующими полями: 29 | 30 | { 31 | TypedArrays: boolean 32 | BlobURLs: boolean 33 | requestAnimationFrame: boolean 34 | pageProtocol: boolean 35 | canvas: boolean 36 | APNG: boolean 37 | } 38 | 39 | Каждое поле имеет значение *true* или *false*. *True* означает встроенную поддержку браузером соответствующей технологии. 40 | Поле `pageProtocol` имеет значение *true*, если страница загружена по протоколу *http* или *https* (на страницах, загруженных по другим протоколам, 41 | работа библиотеки невозможна). 42 | 43 | Библиотека может работать, если все поля, кроме APNG, имеют значения `true`. 44 | 45 | **Rejected**: Не используется. 46 | 47 | #### APNG.parseBuffer(data ArrayBuffer) 48 | Разбирает двоичные данные APNG-файла. 49 | 50 | **Fulfilled** (Animation): Если переданные данные соответствуют корректному APNG, то возвращает объект *Animation* со следующими полями: 51 | 52 | { 53 | // Поля 54 | 55 | width: int // ширина изображения 56 | height: int // высота изображения 57 | numPlays: int // число повторов анимации (0 - бесконечный повтор) 58 | playTime: int // время проигрывания одного цикла анимации в милисекундах 59 | frames: [ // массив фреймов анимации 60 | { 61 | width: int // ширина изображения фрейма 62 | height: int // высота изображения фрейма 63 | left: int // смещение изображения фрейма по горизонтали 64 | top: int // смещение изображения фрейма по вертикали 65 | delay: int // задержка данного фрейма в милисекундах 66 | disposeOp: int // режим восстановления изображения (см. стандарт) 67 | blendOp: int // режим наложения (см. стандарт) 68 | img: HTMLImageElement // изображение 69 | } 70 | ] 71 | 72 | // Методы 73 | 74 | isPlayed(): boolean // проигрывется ли сейчас анимация? 75 | isFinished(): boolean // завершена ли анимация (если numPlays <> 0)? 76 | play() // запустить проигрывание (если не проигрывается и не завершена) 77 | rewind() // вернуть анимацию к началу и остановить 78 | addContext(CanvasRenderingContext2D) // проигрывать анимацию на этом canvas-контексте 79 | // (одна анимация может проигрываться на нескольких контекстах) 80 | removeContext(CanvasRenderingContext2D) // отсоединить данный контекст от анимации 81 | } 82 | 83 | **Rejected** (string): Файл не является корректным APNG, либо произошла ошибка разбора. Возвращает строку с сообщением об ошибке. 84 | 85 | #### APNG.parseURL(url string) 86 | Скачивает изображение по заданному URL и разбирает его. 87 | 88 | **Fulfilled** (Animation): Если загруженные данные соответствуют корректному APNG, то возвращает объект *Animation* (см. *APNG.parseBuffer*). 89 | Для одинаковых URL возвращается один и тот же объект *Animation*. 90 | 91 | **Rejected** (mixed): При загрузке или разборе произошла ошибка. 92 | 93 | #### APNG.animateContext(url string, CanvasRenderingContext2D context) 94 | Скачивает изображение по заданному URL, разбирает его и проигрывает на заданном canvas-контексте. 95 | 96 | **Fulfilled** (Animation): Аналогично результату *APNG.parseURL*. 97 | 98 | **Rejected** (mixed): При загрузке или разборе произошла ошибка. 99 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 David Mzareulyan 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software 4 | and associated documentation files (the "Software"), to deal in the Software without restriction, 5 | including without limitation the rights to use, copy, modify, merge, publish, distribute, 6 | sublicense, and/or sell copies of the Software, and to permit persons to whom the Software 7 | is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial 10 | portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 13 | BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 14 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 15 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | apng-canvas v2.1.0 2 | ============== 3 | 4 | ([README по-русски](https://github.com/davidmz/apng-canvas/blob/master/README_RU.md)) 5 | 6 | Library to display Animated PNG ([Wikipedia](http://en.wikipedia.org/wiki/APNG), [specification](https://wiki.mozilla.org/APNG_Specification)) in a browser using canvas. 7 | 8 | Working demo: https://davidmz.github.io/apng-canvas/ (around 3 Mb of apng files) 9 | 10 | **Please note! API version 2 of the library is incompatible with the API version 1!** 11 | 12 | The library requires support from the following technologies in order to run: 13 | 14 | * [Canvas](http://caniuse.com/#feat=canvas) 15 | * [Typed Arrays](http://caniuse.com/#feat=typedarrays) 16 | * [Blob URLs](http://caniuse.com/#feat=bloburls) 17 | * [requestAnimationFrame](http://caniuse.com/#feat=requestanimationframe) 18 | 19 | These technologies are supported in all modern browsers and IE starting with version 10. 20 | 21 | 22 | Some browsers (at the moment these are Firefox and Safari 8+) have [native support for APNG](http://caniuse.com/#feat=apng). 23 | This library is not required for these browsers. 24 | 25 | Usage example 26 | ----------- 27 | 28 | ```javascript 29 | APNG.ifNeeded().then(function() { 30 | var images = document.querySelectorAll(".apng-image"); 31 | for (var i = 0; i < images.length; i++) APNG.animateImage(images[i]); 32 | }); 33 | ``` 34 | 35 | Limitations 36 | ----------- 37 | 38 | Images are loaded using `XMLHttpRequest`, therefore, the HTML page and APNG image must be located on the same domain 39 | or the correct [CORS](http://www.w3.org/TR/cors/ "Cross-Origin Resource Sharing") header should be provided 40 | (for example, `Access-Control-Allow-Origin: *`). 41 | For the same reason, the library will not work on a local machine (using the protocol `file://`). 42 | 43 | **Important note!** Compression proxies (turbo mode in Opera, "reduce data usage" mode in mobile Chrome, etc.), doesn't know about 44 | APNG format. These proxies transforms APNGs into static images. To prevent it for *your* images, they need to be served with 45 | `Cache-Control: no-transform` HTTP header (see [big article](http://calendar.perfplanet.com/2013/mobile-isp-image-recompression/) about such proxies), 46 | or via HTTPS. 47 | 48 | 49 | API 50 | ----------- 51 | 52 | The library creates a global object **APNG**, which has several methods. 53 | 54 | High-level methods: 55 | 56 | * [APNG.ifNeeded](API.md#user-content-apngifneededignorenativeapng-boolean) 57 | * [APNG.animateImage](API.md#user-content-apnganimateimageimg-htmlimageelement) 58 | * [APNG.releaseCanvas](API.md#user-content-apngreleasecanvascanvas-htmlcanvaselement) 59 | 60 | Low-level methods: 61 | 62 | * [APNG.checkNativeFeatures](API.md#user-content-apngchecknativefeatures) 63 | * [APNG.parseBuffer](API.md#user-content-apngparsebufferdata-arraybuffer) 64 | * [APNG.parseURL](API.md#user-content-apngparseurlurl-string) 65 | * [APNG.animateContext](API.md#user-content-apnganimatecontexturl-string-canvasrenderingcontext2d-context) 66 | 67 | Most methods work asynchronously and return the ES6 *Promise* object. Most browsers have [built-in support](http://caniuse.com/#feat=promises) for it. 68 | For others browsers, library uses [polifill](https://github.com/jakearchibald/es6-promise) (included in the library). 69 | If you have not worked before with Promises, then you should read the [review paper](http://www.html5rocks.com/en/tutorials/es6/promises/) about this technology. 70 | The method description includes values of the Promise result in cases where it is *fulfilled* or *rejected*. 71 | 72 | Build instructions 73 | ----------- 74 | 75 | npm install 76 | gulp build -------------------------------------------------------------------------------- /README_RU.md: -------------------------------------------------------------------------------- 1 | apng-canvas v2.1.0 2 | ============== 3 | 4 | Библиотека для отображения Animated PNG ([Wikipedia](http://en.wikipedia.org/wiki/APNG), [стандарт](https://wiki.mozilla.org/APNG_Specification)) 5 | в браузере при помощи canvas. 6 | 7 | Демонстрация: https://davidmz.github.io/apng-canvas/ (3 Mb of apng files) 8 | 9 | **Внимание! API версии 2 библиотеки несовместимо с API версии 1!** 10 | 11 | Для работы библиотеке требуется поддержка следующих технологий: 12 | 13 | * [Canvas](http://caniuse.com/#feat=canvas) 14 | * [Typed Arrays](http://caniuse.com/#feat=typedarrays) 15 | * [Blob URLs](http://caniuse.com/#feat=bloburls) 16 | * [requestAnimationFrame](http://caniuse.com/#feat=requestanimationframe) 17 | 18 | Эти технологии поддерживаются во всех современных браузерах и в IE начиная с версии 10. 19 | 20 | Некоторые браузеры (на данный момент это Firefox и Safari 8+) имеют [встроенную поддержку APNG](http://caniuse.com/#feat=apng), 21 | для них использование этой библиотеки не обязательно. 22 | 23 | Пример использования 24 | ----------- 25 | 26 | ```javascript 27 | APNG.ifNeeded().then(function() { 28 | var images = document.querySelectorAll(".apng-image"); 29 | for (var i = 0; i < images.length; i++) APNG.animateImage(images[i]); 30 | }); 31 | ``` 32 | 33 | Ограничения 34 | ----------- 35 | 36 | Изображения загружаются при помощи `XMLHttpRequest`, следовательно, HTML-страница и APNG-картинка должны быть расположены на одном домене, 37 | либо сервер, отдающий картинку, должен отдавать правильный [CORS](http://www.w3.org/TR/cors/ "Cross-Origin Resource Sharing")-заголовок 38 | (например, `Access-Control-Allow-Origin: *`). По той же причине библиотека не будет работать с локальной машины (по протоколу `file://`). 39 | 40 | **Важно!** Прокси-сервера, сжимающие трафик (турбо-режим в Опере и Яндекс.Браузере, режим экономии трафика в мобильном Chrome и т. п.), не знают о существовании 41 | формата APNG, и перекодируют его или в статический PNG или в какой-либо другой формат, в результате чего анимация теряется. 42 | Чтобы этого избежать, при отдаче APNG с вашего сайта вы должны использовать HTTP-заголовок `Cache-Control: no-transform` 43 | (см. [обзорную статью](http://calendar.perfplanet.com/2013/mobile-isp-image-recompression/) по этой теме), 44 | либо отдавать такие изображения через HTTPS. 45 | 46 | API 47 | ----------- 48 | 49 | Библиотека создаёт глобальный объект **APNG**, имеющий несколько методов. 50 | 51 | Высокоуровневые методы: 52 | 53 | * [APNG.ifNeeded](API_RU.md#user-content-apngifneededignorenativeapng-boolean) 54 | * [APNG.animateImage](API_RU.md#user-content-apnganimateimageimg-htmlimageelement) 55 | * [APNG.releaseCanvas](API_RU.md#user-content-apngreleasecanvascanvas-htmlcanvaselement) 56 | 57 | Низкоуровневые методы: 58 | 59 | * [APNG.checkNativeFeatures](API_RU.md#user-content-apngchecknativefeatures) 60 | * [APNG.parseBuffer](API_RU.md#user-content-apngparsebufferdata-arraybuffer) 61 | * [APNG.parseURL](API_RU.md#user-content-apngparseurlurl-string) 62 | * [APNG.animateContext](API_RU.md#user-content-apnganimatecontexturl-string-canvasrenderingcontext2d-context) 63 | 64 | Большинство методов работают асинхронно и возвращают объект ES6 *Promise*. Большинство браузеров имеют его [встроенную поддержку](http://caniuse.com/#feat=promises), 65 | для остальных используется [полифилл](https://github.com/jakearchibald/es6-promise), включённый в библиотеку. 66 | Если вы не работали раньше с Promises, то вам поможет [обзорная статья](http://www.html5rocks.com/en/tutorials/es6/promises/) об этой технологии. В описании методов приводятся 67 | значения результата-Promise в случае его выполнения (filfilled) или отказа (rejected). 68 | 69 | Сборка 70 | ----------- 71 | 72 | npm install 73 | gulp build -------------------------------------------------------------------------------- /build/apng-canvas.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * apng-canvas v2.1.2 3 | * 4 | * @copyright 2011-2019 David Mzareulyan 5 | * @link https://github.com/davidmz/apng-canvas 6 | * @license MIT 7 | */ 8 | !function i(o,a,s){function u(n,t){if(!a[n]){if(!o[n]){var e="function"==typeof require&&require;if(!t&&e)return e(n,!0);if(c)return c(n,!0);throw new Error("Cannot find module '"+n+"'")}var r=a[n]={exports:{}};o[n][0].call(r.exports,function(t){var e=o[n][1][t];return u(e||t)},r,r.exports,i,o,a,s)}return a[n].exports}for(var c="function"==typeof require&&require,t=0;ti+r.playTime;)i+=r.playTime;i+=n.delay}else u=!(s=!1)}}},{}],4:[function(t,e,n){"use strict";for(var a=new Uint32Array(256),r=0;r<256;r++){for(var i=r,o=0;o<8;o++)i=1&i?3988292384^i>>>1:i>>>1;a[r]=i}e.exports=function(t,e,n){for(var r=-1,i=e=e||0,o=e+(n=n||t.length-e);i>>8^a[255&(r^t[i])];return-1^r}},{}],5:[function(a,t,e){(function(t){"use strict";var e=a("./support-test"),n=a("./parser"),r=a("./loader"),i=t.APNG={};i.checkNativeFeatures=e.checkNativeFeatures,i.ifNeeded=e.ifNeeded,i.parseBuffer=function(t){return n(t)};var o={};i.parseURL=function(t){return t in o||(o[t]=r(t).then(n)),o[t]},i.animateContext=function(t,e){return i.parseURL(t).then(function(t){return t.addContext(e),t.play(),t})},i.animateImage=function(s){return s.setAttribute("data-is-apng","progress"),i.parseURL(s.src).then(function(t){s.setAttribute("data-is-apng","yes");var e=document.createElement("canvas");e.width=t.width,e.height=t.height,Array.prototype.slice.call(s.attributes).forEach(function(t){-1==["alt","src","usemap","ismap","data-is-apng","width","height"].indexOf(t.nodeName)&&e.setAttributeNode(t.cloneNode(!1))}),e.setAttribute("data-apng-src",s.src),""!=s.alt&&e.appendChild(document.createTextNode(s.alt));var n="",r="",i=0,o="";""!=s.style.width&&"auto"!=s.style.width?n=s.style.width:s.hasAttribute("width")&&(n=s.getAttribute("width")+"px"),""!=s.style.height&&"auto"!=s.style.height?r=s.style.height:s.hasAttribute("height")&&(r=s.getAttribute("height")+"px"),""!=n&&""==r&&(i=parseFloat(n),o=n.match(/\D+$/)[0],r=Math.round(e.height*i/e.width)+o),""!=r&&""==n&&(i=parseFloat(r),o=r.match(/\D+$/)[0],n=Math.round(e.width*i/e.height)+o),e.style.width=n,e.style.height=r;var a=s.parentNode;a.insertBefore(e,s),a.removeChild(s),t.addContext(e.getContext("2d")),t.play()},function(){s.setAttribute("data-is-apng","no")})},i.releaseCanvas=function(t){var e=t.getContext("2d");"_apng_animation"in e&&e._apng_animation.removeContext(e)}}).call(this,"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"./loader":6,"./parser":7,"./support-test":8}],6:[function(t,e,n){"use strict";var i=i||t("es6-promise").Promise;e.exports=function(r){return new i(function(t,e){var n=new XMLHttpRequest;n.open("GET",r),n.responseType="arraybuffer",n.onload=function(){200==this.status?t(this.response):e(this)},n.send()})}},{"es6-promise":1}],7:[function(t,e,n){"use strict";var r=r||t("es6-promise").Promise,m=t("./animation"),o=t("./crc32"),g=new Uint8Array([137,80,78,71,13,10,26,10]);e.exports=function(t){var A=new Uint8Array(t);return new r(function(t,e){for(var n=0;n>>0;for(var r=1;r<4;r++)n+=t[r+e]<<8*(3-r);return n},_=function(t,e){for(var n=0,r=0;r<2;r++)n+=t[r+e]<<8*(1-r);return n},b=function(t,e){return t[e]},E=function(t,e,n){var r=new Uint8Array(n);return r.set(t.subarray(e,e+n)),r},a=function(t,e,n){var r=Array.prototype.slice.call(t.subarray(e,e+n));return String.fromCharCode.apply(String,r)},P=function(t){return[t>>>24&255,t>>>16&255,t>>>8&255,255&t]},x=function(t,e){var n=t.length+e.length,r=new Uint8Array(new ArrayBuffer(n+8));r.set(P(e.length),0),r.set(function(t){for(var e=[],n=0;n 0) { 46 | var dat = contexts[0].getImageData(0, 0, this.width, this.height); 47 | ctx.putImageData(dat, 0, 0); 48 | } 49 | contexts.push(ctx); 50 | ctx['_apng_animation'] = this; 51 | }; 52 | 53 | /** 54 | * Remove canvas context from animation 55 | * @param {CanvasRenderingContext2D} ctx 56 | * @return {void} 57 | */ 58 | this.removeContext = function (ctx) { 59 | var idx = contexts.indexOf(ctx); 60 | if (idx === -1) { 61 | return; 62 | } 63 | contexts.splice(idx, 1); 64 | if (contexts.length === 0) { 65 | this.rewind(); 66 | } 67 | if ('_apng_animation' in ctx) { 68 | delete ctx['_apng_animation']; 69 | } 70 | }; 71 | 72 | //noinspection JSUnusedGlobalSymbols 73 | /** 74 | * Is animation played? 75 | * @return {boolean} 76 | */ 77 | this.isPlayed = function () { return played; }; 78 | 79 | //noinspection JSUnusedGlobalSymbols 80 | /** 81 | * Is animation finished? 82 | * @return {boolean} 83 | */ 84 | this.isFinished = function () { return finished; }; 85 | 86 | // Private 87 | 88 | var ani = this, 89 | nextRenderTime = 0, 90 | fNum = 0, 91 | prevF = null, 92 | played = false, 93 | finished = false, 94 | contexts = []; 95 | 96 | var tick = function (now) { 97 | while (played && nextRenderTime <= now) renderFrame(now); 98 | if (played) requestAnimationFrame(tick); 99 | }; 100 | 101 | var renderFrame = function (now) { 102 | var f = fNum++ % ani.frames.length; 103 | var frame = ani.frames[f]; 104 | 105 | if (!(ani.numPlays == 0 || fNum / ani.frames.length <= ani.numPlays)) { 106 | played = false; 107 | finished = true; 108 | return; 109 | } 110 | 111 | if (f == 0) { 112 | contexts.forEach(function (ctx) {ctx.clearRect(0, 0, ani.width, ani.height);}); 113 | prevF = null; 114 | if (frame.disposeOp == 2) frame.disposeOp = 1; 115 | } 116 | 117 | if (prevF && prevF.disposeOp == 1) { 118 | contexts.forEach(function (ctx) {ctx.clearRect(prevF.left, prevF.top, prevF.width, prevF.height);}); 119 | } else if (prevF && prevF.disposeOp == 2) { 120 | contexts.forEach(function (ctx) {ctx.putImageData(prevF.iData, prevF.left, prevF.top);}); 121 | } 122 | prevF = frame; 123 | prevF.iData = null; 124 | if (prevF.disposeOp == 2) { 125 | prevF.iData = contexts[0].getImageData(frame.left, frame.top, frame.width, frame.height); 126 | } 127 | if (frame.blendOp == 0) { 128 | contexts.forEach(function (ctx) {ctx.clearRect(frame.left, frame.top, frame.width, frame.height);}); 129 | } 130 | contexts.forEach(function (ctx) {ctx.drawImage(frame.img, frame.left, frame.top);}); 131 | 132 | if (nextRenderTime == 0) nextRenderTime = now; 133 | while (now > nextRenderTime + ani.playTime) nextRenderTime += ani.playTime; 134 | nextRenderTime += frame.delay; 135 | }; 136 | }; 137 | 138 | module.exports = Animation; -------------------------------------------------------------------------------- /src/crc32.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var table = new Uint32Array(256); 4 | 5 | for (var i = 0; i < 256; i++) { 6 | var c = i; 7 | for (var k = 0; k < 8; k++) c = (c & 1) ? 0xEDB88320 ^ (c >>> 1) : c >>> 1; 8 | table[i] = c; 9 | } 10 | 11 | /** 12 | * 13 | * @param {Uint8Array} bytes 14 | * @param {int} start 15 | * @param {int} length 16 | * @return {int} 17 | */ 18 | module.exports = function (bytes, start, length) { 19 | start = start || 0; 20 | length = length || (bytes.length - start); 21 | var crc = -1; 22 | for (var i = start, l = start + length; i < l; i++) { 23 | crc = ( crc >>> 8 ) ^ table[( crc ^ bytes[i] ) & 0xFF]; 24 | } 25 | return crc ^ (-1); 26 | }; 27 | -------------------------------------------------------------------------------- /src/loader.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var Promise = Promise || require('es6-promise').Promise; 4 | 5 | module.exports = function (url) { 6 | return new Promise(function (resolve, reject) { 7 | var xhr = new XMLHttpRequest(); 8 | xhr.open('GET', url); 9 | xhr.responseType = 'arraybuffer'; 10 | xhr.onload = function () { 11 | if (this.status == 200) { 12 | resolve(this.response); 13 | } else { 14 | reject(this); 15 | } 16 | }; 17 | xhr.send(); 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * API: 3 | * 4 | * ifNeeded([ignoreNativeAPNG bool]) → Promise() 5 | * animateImage(img HTMLImageElement) → Promise() 6 | * 7 | * animateContext(url String, CanvasRenderingContext2D context) → Promise(Animation) 8 | * parseBuffer(ArrayBuffer) → Promise(Animation) 9 | * parseURL(url String) → Promise(Animation) 10 | * checkNativeFeatures() → Promise(features) 11 | */ 12 | "use strict"; 13 | 14 | var support = require("./support-test"); 15 | var parseAPNG = require("./parser"); 16 | var loadUrl = require('./loader'); 17 | 18 | var APNG = global.APNG = {}; 19 | 20 | APNG.checkNativeFeatures = support.checkNativeFeatures; 21 | APNG.ifNeeded = support.ifNeeded; 22 | 23 | /** 24 | * @param {ArrayBuffer} buffer 25 | * @return {Promise} 26 | */ 27 | APNG.parseBuffer = function (buffer) { return parseAPNG(buffer); }; 28 | 29 | var url2promise = {}; 30 | /** 31 | * @param {String} url 32 | * @return {Promise} 33 | */ 34 | APNG.parseURL = function (url) { 35 | if (!(url in url2promise)) url2promise[url] = loadUrl(url).then(parseAPNG); 36 | return url2promise[url]; 37 | }; 38 | 39 | /** 40 | * @param {String} url 41 | * @param {CanvasRenderingContext2D} context 42 | * @return {Promise} 43 | */ 44 | APNG.animateContext = function (url, context) { 45 | return APNG.parseURL(url).then(function (a) { 46 | a.addContext(context); 47 | a.play(); 48 | return a; 49 | }); 50 | }; 51 | 52 | /** 53 | * @param {HTMLImageElement} img 54 | * @return {Promise} 55 | */ 56 | APNG.animateImage = function (img) { 57 | img.setAttribute("data-is-apng", "progress"); 58 | return APNG.parseURL(img.src).then( 59 | function (anim) { 60 | img.setAttribute("data-is-apng", "yes"); 61 | var canvas = document.createElement("canvas"); 62 | canvas.width = anim.width; 63 | canvas.height = anim.height; 64 | Array.prototype.slice.call(img.attributes).forEach(function (attr) { 65 | if (["alt", "src", "usemap", "ismap", "data-is-apng", "width", "height"].indexOf(attr.nodeName) == -1) { 66 | canvas.setAttributeNode(attr.cloneNode(false)); 67 | } 68 | }); 69 | canvas.setAttribute("data-apng-src", img.src); 70 | if (img.alt != "") canvas.appendChild(document.createTextNode(img.alt)); 71 | 72 | var imgWidth = "", imgHeight = "", val = 0, unit = ""; 73 | 74 | if (img.style.width != "" && img.style.width != "auto") { 75 | imgWidth = img.style.width; 76 | } else if (img.hasAttribute("width")) { 77 | imgWidth = img.getAttribute("width") + "px"; 78 | } 79 | if (img.style.height != "" && img.style.height != "auto") { 80 | imgHeight = img.style.height; 81 | } else if (img.hasAttribute("height")) { 82 | imgHeight = img.getAttribute("height") + "px"; 83 | } 84 | if (imgWidth != "" && imgHeight == "") { 85 | val = parseFloat(imgWidth); 86 | unit = imgWidth.match(/\D+$/)[0]; 87 | imgHeight = Math.round(canvas.height * val / canvas.width) + unit; 88 | } 89 | if (imgHeight != "" && imgWidth == "") { 90 | val = parseFloat(imgHeight); 91 | unit = imgHeight.match(/\D+$/)[0]; 92 | imgWidth = Math.round(canvas.width * val / canvas.height) + unit; 93 | } 94 | canvas.style.width = imgWidth; 95 | canvas.style.height = imgHeight; 96 | 97 | var p = img.parentNode; 98 | p.insertBefore(canvas, img); 99 | p.removeChild(img); 100 | anim.addContext(canvas.getContext("2d")); 101 | anim.play(); 102 | }, 103 | function () { 104 | img.setAttribute("data-is-apng", "no"); 105 | }); 106 | }; 107 | 108 | /** 109 | * @param {HTMLCanvasElement} canvas 110 | * @return {void} 111 | */ 112 | APNG.releaseCanvas = function(canvas) { 113 | var ctx = canvas.getContext("2d"); 114 | if ('_apng_animation' in ctx) { 115 | ctx['_apng_animation'].removeContext(ctx); 116 | } 117 | }; -------------------------------------------------------------------------------- /src/parser.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var Promise = Promise || require('es6-promise').Promise; 4 | var Animation = require('./animation'); 5 | var crc32 = require('./crc32'); 6 | 7 | // "\x89PNG\x0d\x0a\x1a\x0a" 8 | var PNG_SIGNATURE_BYTES = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); 9 | 10 | /** 11 | * @param {ArrayBuffer} buffer 12 | * @return {Promise} 13 | */ 14 | module.exports = function (buffer) { 15 | var bytes = new Uint8Array(buffer); 16 | return new Promise(function (resolve, reject) { 17 | 18 | for (var i = 0; i < PNG_SIGNATURE_BYTES.length; i++) { 19 | if (PNG_SIGNATURE_BYTES[i] != bytes[i]) { 20 | reject("Not a PNG file (invalid file signature)"); 21 | return; 22 | } 23 | } 24 | 25 | // fast animation test 26 | var isAnimated = false; 27 | parseChunks(bytes, function (type) { 28 | if (type == "acTL") { 29 | isAnimated = true; 30 | return false; 31 | } 32 | return true; 33 | }); 34 | if (!isAnimated) { 35 | reject("Not an animated PNG"); 36 | return; 37 | } 38 | 39 | var 40 | preDataParts = [], 41 | postDataParts = [], 42 | headerDataBytes = null, 43 | frame = null, 44 | anim = new Animation(); 45 | 46 | parseChunks(bytes, function (type, bytes, off, length) { 47 | switch (type) { 48 | case "IHDR": 49 | headerDataBytes = bytes.subarray(off + 8, off + 8 + length); 50 | anim.width = readDWord(bytes, off + 8); 51 | anim.height = readDWord(bytes, off + 12); 52 | break; 53 | case "acTL": 54 | anim.numPlays = readDWord(bytes, off + 8 + 4); 55 | break; 56 | case "fcTL": 57 | if (frame) anim.frames.push(frame); 58 | frame = {}; 59 | frame.width = readDWord(bytes, off + 8 + 4); 60 | frame.height = readDWord(bytes, off + 8 + 8); 61 | frame.left = readDWord(bytes, off + 8 + 12); 62 | frame.top = readDWord(bytes, off + 8 + 16); 63 | var delayN = readWord(bytes, off + 8 + 20); 64 | var delayD = readWord(bytes, off + 8 + 22); 65 | if (delayD == 0) delayD = 100; 66 | frame.delay = 1000 * delayN / delayD; 67 | // see http://mxr.mozilla.org/mozilla/source/gfx/src/shared/gfxImageFrame.cpp#343 68 | if (frame.delay <= 10) frame.delay = 100; 69 | anim.playTime += frame.delay; 70 | frame.disposeOp = readByte(bytes, off + 8 + 24); 71 | frame.blendOp = readByte(bytes, off + 8 + 25); 72 | frame.dataParts = []; 73 | break; 74 | case "fdAT": 75 | if (frame) frame.dataParts.push(bytes.subarray(off + 8 + 4, off + 8 + length)); 76 | break; 77 | case "IDAT": 78 | if (frame) frame.dataParts.push(bytes.subarray(off + 8, off + 8 + length)); 79 | break; 80 | case "IEND": 81 | postDataParts.push(subBuffer(bytes, off, 12 + length)); 82 | break; 83 | default: 84 | preDataParts.push(subBuffer(bytes, off, 12 + length)); 85 | } 86 | }); 87 | 88 | if (frame) anim.frames.push(frame); 89 | 90 | if (anim.frames.length == 0) { 91 | reject("Not an animated PNG"); 92 | return; 93 | } 94 | 95 | // creating images 96 | var createdImages = 0; 97 | var preBlob = new Blob(preDataParts), postBlob = new Blob(postDataParts); 98 | for (var f = 0; f < anim.frames.length; f++) { 99 | frame = anim.frames[f]; 100 | 101 | var bb = []; 102 | bb.push(PNG_SIGNATURE_BYTES); 103 | headerDataBytes.set(makeDWordArray(frame.width), 0); 104 | headerDataBytes.set(makeDWordArray(frame.height), 4); 105 | bb.push(makeChunkBytes("IHDR", headerDataBytes)); 106 | bb.push(preBlob); 107 | for (var j = 0; j < frame.dataParts.length; j++) { 108 | bb.push(makeChunkBytes("IDAT", frame.dataParts[j])); 109 | } 110 | bb.push(postBlob); 111 | var url = URL.createObjectURL(new Blob(bb, {"type": "image/png"})); 112 | delete frame.dataParts; 113 | bb = null; 114 | 115 | /** 116 | * Using "createElement" instead of "new Image" because of bug in Chrome 27 117 | * https://code.google.com/p/chromium/issues/detail?id=238071 118 | * http://stackoverflow.com/questions/16377375/using-canvas-drawimage-in-chrome-extension-content-script/16378270 119 | */ 120 | frame.img = document.createElement('img'); 121 | frame.img.onload = function () { 122 | URL.revokeObjectURL(this.src); 123 | createdImages++; 124 | if (createdImages == anim.frames.length) { 125 | resolve(anim); 126 | } 127 | }; 128 | frame.img.onerror = function () { 129 | reject("Image creation error"); 130 | }; 131 | frame.img.src = url; 132 | } 133 | }); 134 | }; 135 | 136 | /** 137 | * @param {Uint8Array} bytes 138 | * @param {function(string, Uint8Array, int, int)} callback 139 | */ 140 | var parseChunks = function (bytes, callback) { 141 | var off = 8; 142 | do { 143 | var length = readDWord(bytes, off); 144 | var type = readString(bytes, off + 4, 4); 145 | var res = callback(type, bytes, off, length); 146 | off += 12 + length; 147 | } while (res !== false && type != "IEND" && off < bytes.length); 148 | }; 149 | 150 | /** 151 | * @param {Uint8Array} bytes 152 | * @param {int} off 153 | * @return {int} 154 | */ 155 | var readDWord = function (bytes, off) { 156 | var x = 0; 157 | // Force the most-significant byte to unsigned. 158 | x += ((bytes[0 + off] << 24 ) >>> 0); 159 | for (var i = 1; i < 4; i++) x += ( (bytes[i + off] << ((3 - i) * 8)) ); 160 | return x; 161 | }; 162 | 163 | /** 164 | * @param {Uint8Array} bytes 165 | * @param {int} off 166 | * @return {int} 167 | */ 168 | var readWord = function (bytes, off) { 169 | var x = 0; 170 | for (var i = 0; i < 2; i++) x += (bytes[i + off] << ((1 - i) * 8)); 171 | return x; 172 | }; 173 | 174 | /** 175 | * @param {Uint8Array} bytes 176 | * @param {int} off 177 | * @return {int} 178 | */ 179 | var readByte = function (bytes, off) { 180 | return bytes[off]; 181 | }; 182 | 183 | /** 184 | * @param {Uint8Array} bytes 185 | * @param {int} start 186 | * @param {int} length 187 | * @return {Uint8Array} 188 | */ 189 | var subBuffer = function (bytes, start, length) { 190 | var a = new Uint8Array(length); 191 | a.set(bytes.subarray(start, start + length)); 192 | return a; 193 | }; 194 | 195 | var readString = function (bytes, off, length) { 196 | var chars = Array.prototype.slice.call(bytes.subarray(off, off + length)); 197 | return String.fromCharCode.apply(String, chars); 198 | }; 199 | 200 | var makeDWordArray = function (x) { 201 | return [(x >>> 24) & 0xff, (x >>> 16) & 0xff, (x >>> 8) & 0xff, x & 0xff]; 202 | }; 203 | var makeStringArray = function (x) { 204 | var res = []; 205 | for (var i = 0; i < x.length; i++) res.push(x.charCodeAt(i)); 206 | return res; 207 | }; 208 | /** 209 | * @param {string} type 210 | * @param {Uint8Array} dataBytes 211 | * @return {Uint8Array} 212 | */ 213 | var makeChunkBytes = function (type, dataBytes) { 214 | var crcLen = type.length + dataBytes.length; 215 | var bytes = new Uint8Array(new ArrayBuffer(crcLen + 8)); 216 | bytes.set(makeDWordArray(dataBytes.length), 0); 217 | bytes.set(makeStringArray(type), 4); 218 | bytes.set(dataBytes, 8); 219 | var crc = crc32(bytes, 4, crcLen); 220 | bytes.set(makeDWordArray(crc), crcLen + 4); 221 | return bytes; 222 | }; 223 | 224 | -------------------------------------------------------------------------------- /src/support-test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var Promise = Promise || require('es6-promise').Promise; 4 | 5 | var oncePromise = function (foo) { 6 | var promise = null; 7 | return function (callback) { 8 | if (!promise) promise = new Promise(foo); 9 | if (callback) promise.then(callback); 10 | return promise; 11 | }; 12 | }; 13 | 14 | var checkNativeFeatures = oncePromise(function (resolve) { 15 | var canvas = document.createElement("canvas"); 16 | var result = { 17 | TypedArrays: ("ArrayBuffer" in global), 18 | BlobURLs: ("URL" in global), 19 | requestAnimationFrame: ("requestAnimationFrame" in global), 20 | pageProtocol: (location.protocol == "http:" || location.protocol == "https:"), 21 | canvas: ("getContext" in document.createElement("canvas")), 22 | APNG: false 23 | }; 24 | 25 | if (result.canvas) { 26 | // see http://eligrey.com/blog/post/apng-feature-detection 27 | var img = new Image(); 28 | img.onload = function () { 29 | var ctx = canvas.getContext("2d"); 30 | ctx.drawImage(img, 0, 0); 31 | result.APNG = (ctx.getImageData(0, 0, 1, 1).data[3] === 0); 32 | resolve(result); 33 | }; 34 | // frame 1 (skipped on apng-supporting browsers): [0, 0, 0, 255] 35 | // frame 2: [0, 0, 0, 0] 36 | img.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACGFjV" + 37 | "EwAAAABAAAAAcMq2TYAAAANSURBVAiZY2BgYPgPAAEEAQB9ssjfAAAAGmZjVEwAAAAAAAAAAQAAAAEAAA" + 38 | "AAAAAAAAD6A+gBAbNU+2sAAAARZmRBVAAAAAEImWNgYGBgAAAABQAB6MzFdgAAAABJRU5ErkJggg=="; 39 | } else { 40 | resolve(result); 41 | } 42 | }); 43 | 44 | /** 45 | * @param {boolean} [ignoreNativeAPNG] 46 | * @return {Promise} 47 | */ 48 | var ifNeeded = function (ignoreNativeAPNG) { 49 | if (typeof ignoreNativeAPNG == 'undefined') ignoreNativeAPNG = false; 50 | return checkNativeFeatures().then(function (features) { 51 | if (features.APNG && !ignoreNativeAPNG) { 52 | reject(); 53 | } else { 54 | var ok = true; 55 | for (var k in features) if (features.hasOwnProperty(k) && k != 'APNG') { 56 | ok = ok && features[k]; 57 | } 58 | } 59 | }); 60 | }; 61 | 62 | module.exports = { 63 | checkNativeFeatures: checkNativeFeatures, 64 | ifNeeded: ifNeeded 65 | }; 66 | --------------------------------------------------------------------------------