diff --git a/.eslintrc.json b/.eslintrc.json index 36b7885..75587e8 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,5 +1,6 @@ { "extends": "eslint:recommended", + "parser": "babel-eslint", "env": { "es6": true, "node": true @@ -15,4 +16,4 @@ "quotes": ["error", "single"], "semi": ["error", "always"] } -} \ No newline at end of file +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c65ff82..ac0966e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,27 +14,27 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [8, 10, 12, 14, 16, 18] + node-version: [12, 14, 16, 18, 20] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - run: npm install - run: npm run fetch - run: npm test - run: npm run lint - - name: inspect tarball - run: npm pack + - name: Inspect tarball + run: npm pack --dry-run publish: runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/') && github.event_name != 'pull_request' needs: test steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Use Node.js 14 - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: 14 registry-url: https://registry.npmjs.org/ @@ -46,7 +46,7 @@ jobs: NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} - name: Output logs if: ${{ always() }} - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: npm-logs path: /home/runner/.npm/_logs/** diff --git a/.gitignore b/.gitignore index 107d7e8..dc0e3a3 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,4 @@ temp/ # not source code libheif/ +libheif-wasm/ diff --git a/README.md b/README.md index 004ac4a..97a3ef8 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ > An Emscripten build of [`libheif`](https://github.com/strukturag/libheif) distributed as an npm module for Node.JS and the browser. [![github actions test][github-actions-test.svg]][github-actions-test.link] +[![jsdelivr][jsdelivr.svg]][jsdelivr.link] [![npm-downloads][npm-downloads.svg]][npm.link] [![npm-version][npm-version.svg]][npm.link] @@ -11,6 +12,8 @@ [npm-downloads.svg]: https://img.shields.io/npm/dm/libheif-js.svg [npm.link]: https://www.npmjs.com/package/libheif-js [npm-version.svg]: https://img.shields.io/npm/v/libheif-js.svg +[jsdelivr.svg]: https://img.shields.io/jsdelivr/npm/hm/libheif-js?color=bd33a4 +[jsdelivr.link]: https://www.jsdelivr.com/package/npm/libheif-js This module will respect the major and minor versions of the included `libheif`, with the patch version representing changes in this module itself. For the exact version of `libheif`, please see the [install script](scripts/install.js). @@ -20,6 +23,103 @@ This module will respect the major and minor versions of the included `libheif`, npm install libheif-js ``` +## Usage + +Starting with version 1.17, there are multiple variants of `libheif` that you can use: + +* The default is still the classic pure-javascript implementation (for backwards compatibility, of course). You can still bundle this into your project with your bundler of choice. + ```js + const libheif = require('libheif-js'); + ``` +* There is a `wasm` version available for use in NodeJS. This version will dymanically load the `.wasm` binary at runtime. While you may try to run this through a bundler, you are on your own for making it work. + ```js + const libheif = require('libheif-js/wasm'); + ``` +* There is also a `wasm` version that is pre-bundled for you, which includes the `.wasm` binary inside the `.js` bundle. You will have a much easier time using this in your browser bundle project. + ```js + const libheif = require('libheif-js/wasm-bundle'); + ``` + +If you'd like to include this module directly into an `html` page using a ` + ``` +* Use the wasm bundle, exposing a `libheif` global: + ```html + + ``` +* Use the ES Module version, which now works in all major browsers and you should try it: + ```html + + ``` + +In all cases, you can use this sample code to decode an image: + +```js +const file = fs.readFileSync('./temp/0002.heic'); + +const decoder = new libheif.HeifDecoder(); +const data = decoder.decode(file); +// data in an array holding all images inside the heic file + +const image = data[0]; +const width = image.get_width(); +const height = image.get_height(); +``` + +In NodeJS, you might use this decoded data with other libraries, such as `pngjs`: + +```js +const { PNG } = require('pngjs'); + +const arrayBuffer = await new Promise((resolve, reject) => { + image.display({ data: new Uint8ClampedArray(width*height*4), width, height }, (displayData) => { + if (!displayData) { + return reject(new Error('HEIF processing error')); + } + + resolve(displayData.data.buffer); + }); +}); + +const imageData = { width, height, data: arrayBuffer }; + +const png = new PNG({ width: imageData.width, height: imageData.height }); +png.data = Buffer.from(imageData.data); + +const pngBuffer = PNG.sync.write(png); +``` + +In the browser, you might use this decoded data with `canvas` to display or convert the image: + +```js +const canvas = document.createElement('canvas'); + +canvas.width = width; +canvas.height = height; + +const context = canvas.getContext('2d'); +const imageData = context.createImageData(width, height); + +await new Promise((resolve, reject) => { + image.display(imageData, (displayData) => { + if (!displayData) { + return reject(new Error('HEIF processing error')); + } + + resolve(); + }); +}); + +context.putImageData(imageData, 0, 0); +``` + ## Related This module contains the low-level `libheif` implementation. For more user-friendly functionality, check out these projects: diff --git a/index.js b/index.js new file mode 100644 index 0000000..3381a4c --- /dev/null +++ b/index.js @@ -0,0 +1 @@ +module.exports = require('./libheif/libheif.js')(); diff --git a/package.json b/package.json index cbb5bf6..b790790 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,11 @@ { "name": "libheif-js", - "version": "1.15.1", + "version": "1.17.1", "description": "Emscripten distribution of libheif for Node.JS and the browser", - "main": "libheif/libheif.js", + "main": "index.js", "scripts": { "pretest": "npm run -s images", - "test": "mocha test/**/*.test.js", + "test": "mocha test/**/*.test.js --timeout 4000", "fetch": "node scripts/install.js", "images": "node scripts/images.js", "inspect": "node scripts/convert.js < temp/0002.heic > out.png", @@ -16,7 +16,11 @@ "url": "git+https://github.com/catdad-experiments/libheif-js.git" }, "files": [ - "libheif" + "index.js", + "wasm.js", + "wasm-bundle.js", + "libheif", + "libheif-wasm" ], "author": "Kiril Vatev ", "license": "LGPL-3.0", @@ -25,14 +29,18 @@ }, "homepage": "https://github.com/catdad-experiments/libheif-js#readme", "devDependencies": { + "babel-eslint": "^10.1.0", "chai": "^4.2.0", + "esbuild": "^0.19.5", "eslint": "^5.16.0", "fs-extra": "^8.1.0", + "gunzip-maybe": "^1.4.2", "mocha": "^7.0.0", "node-fetch": "^2.6.0", "pixelmatch": "^5.2.1", "pngjs": "^3.4.0", - "rootrequire": "^1.0.0" + "rootrequire": "^1.0.0", + "tar-stream": "^3.1.6" }, "engines": { "node": ">=8.0.0" @@ -44,6 +52,7 @@ "decoder", "node", "browser", - "emscripten" + "emscripten", + "wasm" ] } diff --git a/scripts/bundle.js b/scripts/bundle.js new file mode 100644 index 0000000..cc56c21 --- /dev/null +++ b/scripts/bundle.js @@ -0,0 +1,4 @@ +import libheif from '../libheif-wasm/libheif.js'; +import wasmBinary from '../libheif-wasm/libheif.wasm'; + +export default (opts = {}) => libheif({ ...opts, wasmBinary }); diff --git a/scripts/install.js b/scripts/install.js index f7773a7..408e9cb 100644 --- a/scripts/install.js +++ b/scripts/install.js @@ -3,30 +3,89 @@ const path = require('path'); const fs = require('fs-extra'); const fetch = require('node-fetch'); const root = require('rootrequire'); +const tar = require('tar-stream'); +const gunzip = require('gunzip-maybe'); -const libheifDir = path.resolve(root, 'libheif'); -const libheif = path.resolve(libheifDir, 'libheif.js'); -const libheifLicense = path.resolve(libheifDir, 'LICENSE'); +const esbuild = require('esbuild'); -const version = 'v1.15.1'; +const version = 'v1.17.1'; const base = `https://github.com/catdad-experiments/libheif-emscripten/releases/download/${version}`; -const lib = `${base}/libheif.js`; -const license = `${base}/LICENSE`; +const tarball = `${base}/libheif.tar.gz`; -const response = async url => { +const getStream = async url => { const res = await fetch(url); if (!res.ok) { throw new Error(`failed response: ${res.status} ${res.statusText}`); } - return await res.buffer(); + return res.body; +}; + +const autoReadStream = async stream => { + let result = Buffer.from(''); + + for await (const data of stream) { + result = Buffer.concat([result, data]); + } + + return result; }; (async () => { - await fs.outputFile(libheif, await response(lib)); - await fs.outputFile(libheifLicense, await response(license)); + await fs.remove(path.resolve(root, 'libheif')); + await fs.remove(path.resolve(root, 'libheif-wasm')); + + for await (const entry of (await getStream(tarball)).pipe(gunzip()).pipe(tar.extract())) { + const basedir = entry.header.name.split('/')[0]; + + if (entry.header.type === 'file' && ['libheif', 'libheif-wasm'].includes(basedir)) { + const outfile = path.resolve(root, entry.header.name); + console.log(` writing "${outfile}"`); + await fs.outputFile(outfile, await autoReadStream(entry)); + } else { + await autoReadStream(entry); + } + } + + const buildOptions = { + entryPoints: [path.resolve(root, 'scripts/bundle.js')], + bundle: true, + minify: true, + external: ['fs', 'path', 'require'], + loader: { + '.wasm': 'binary' + }, + platform: 'neutral' + }; + + await esbuild.build({ + ...buildOptions, + outfile: path.resolve(root, 'libheif-wasm/libheif-bundle.js'), + format: 'iife', + globalName: 'libheif', + footer: { + // hack to support a single bundle as a node cjs module + // and a browser