-
Notifications
You must be signed in to change notification settings - Fork 28
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #33 from catdad-experiments/canvas-browser-impleme…
…ntation add a canvas-based browser implementation for encoding jpeg/png images
- Loading branch information
Showing
10 changed files
with
294 additions
and
170 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
const decode = require('heic-decode'); | ||
const formats = require('./formats-browser.js'); | ||
const { one, all } = require('./lib.js')(decode, formats); | ||
|
||
module.exports = one; | ||
module.exports.all = all; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
const initializeCanvas = ({ width, height }) => { | ||
const canvas = document.createElement('canvas'); | ||
canvas.width = width; | ||
canvas.height = height; | ||
|
||
return canvas; | ||
}; | ||
|
||
const convert = async ({ data, width, height }, ...blobArgs) => { | ||
const canvas = initializeCanvas({ width, height }); | ||
|
||
const ctx = canvas.getContext('2d'); | ||
ctx.putImageData(new ImageData(data, width, height), 0, 0); | ||
|
||
const blob = await new Promise((resolve, reject) => { | ||
canvas.toBlob(blob => { | ||
if (blob) { | ||
return resolve(blob); | ||
} | ||
|
||
return reject(new Error('failed to convert the image')); | ||
}, ...blobArgs); | ||
}); | ||
|
||
const arrayBuffer = await blob.arrayBuffer(); | ||
|
||
return new Uint8Array(arrayBuffer); | ||
}; | ||
|
||
module.exports = { | ||
JPEG: async ({ data, width, height, quality }) => await convert({ data, width, height }, 'image/jpeg', quality), | ||
PNG: async ({ data, width, height }) => await convert({ data, width, height }, 'image/png') | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
const jpegJs = require('jpeg-js'); | ||
const { PNG } = require('pngjs'); | ||
|
||
module.exports = {}; | ||
|
||
module.exports.JPEG = ({ data, width, height, quality }) => jpegJs.encode({ data, width, height }, Math.floor(quality * 100)).data; | ||
|
||
module.exports.PNG = ({ data, width, height }) => { | ||
const png = new PNG({ width, height }); | ||
png.data = data; | ||
|
||
return PNG.sync.write(png, { | ||
width: width, | ||
height: height, | ||
deflateLevel: 9, | ||
deflateStrategy: 3, | ||
filterType: -1, | ||
colorType: 6, | ||
inputHasAlpha: true | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
const decode = require('heic-decode'); | ||
const { one, all } = require('./lib.js')(decode); | ||
const formats = require('./formats-node.js'); | ||
const { one, all } = require('./lib.js')(decode, formats); | ||
|
||
module.exports = one; | ||
module.exports.all = all; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
// jsdom is not supported in node < 16, and... | ||
// that's fine, because we don't need to run the browser | ||
// tests everywhere, just at least in one env | ||
if (Number(process.versions.node.split('.')[0]) < 16) { | ||
return; | ||
} | ||
|
||
const JSDOM = require('jsdom').JSDOM; | ||
const canvas = require('canvas'); | ||
const runTests = require('./run-tests.js'); | ||
|
||
describe('heic-convert (browser image encoding)', () => { | ||
before(() => { | ||
const { window } = new JSDOM(``, { | ||
pretendToBeVisual: true | ||
}); | ||
|
||
global.window = window; | ||
global.document = window.document; | ||
global.ImageData = canvas.ImageData; | ||
|
||
// okay, now we are getting hacky... jsdom folks got into a | ||
// fight when talking about implementing this spec, which | ||
// is now broadly supported across all evergreen browsers | ||
// https://github.com/jsdom/jsdom/issues/2555 | ||
global.window.Blob.prototype.arrayBuffer = async function() { | ||
const blob = this; | ||
const fileReader = new window.FileReader(); | ||
|
||
var arrayBuffer = new Promise(r => { | ||
fileReader.onload = function(event) { | ||
r(event.target.result); | ||
}; | ||
|
||
fileReader.readAsArrayBuffer(blob); | ||
}); | ||
|
||
return arrayBuffer; | ||
}; | ||
}); | ||
|
||
after(() => { | ||
global.window.close(); | ||
delete global.window; | ||
}); | ||
|
||
runTests(require('../browser')); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,148 +1,22 @@ | ||
/* eslint-env mocha */ | ||
const { promisify } = require('util'); | ||
const fs = require('fs'); | ||
const path = require('path'); | ||
const runTests = require('./run-tests.js'); | ||
|
||
const root = require('rootrequire'); | ||
const { fromBuffer: filetype } = require('file-type'); | ||
const { expect } = require('chai'); | ||
const toUint8 = require('buffer-to-uint8array'); | ||
const pixelmatch = require('pixelmatch'); | ||
const { PNG } = require('pngjs'); | ||
const jpegJs = require('jpeg-js'); | ||
|
||
const convert = require('../'); | ||
|
||
const readFile = promisify(fs.readFile); | ||
|
||
describe('heic-convert', () => { | ||
it('exports a function', () => { | ||
expect(convert).to.be.a('function'); | ||
}); | ||
|
||
const decode = { | ||
'image/png': buffer => PNG.sync.read(buffer), | ||
'image/jpeg': buffer => jpegJs.decode(buffer) | ||
}; | ||
|
||
const readControl = async name => { | ||
const buffer = await readFile(path.resolve(root, `temp/${name}`)); | ||
return decode['image/png'](buffer); | ||
}; | ||
|
||
const compare = (expected, actual, width, height, errString = 'actual image did not match control image') => { | ||
const result = pixelmatch(toUint8(Buffer.from(expected)), toUint8(Buffer.from(actual)), null, width, height, { | ||
threshold: 0.1 | ||
}); | ||
|
||
// allow 5% of pixels to be different | ||
expect(result).to.be.below(width * height * 0.05, errString); | ||
}; | ||
|
||
const assertImage = async (buffer, mime, control) => { | ||
const type = await filetype(buffer); | ||
expect(type.mime).to.equal(mime); | ||
|
||
const actual = decode[mime](buffer); | ||
|
||
expect(actual.width).to.equal(control.width); | ||
expect(actual.height).to.equal(control.height); | ||
|
||
compare(control.data, actual.data, control.width, control.height); | ||
}; | ||
|
||
it('converts known image to jpeg', async () => { | ||
const control = await readControl('0002-control.png'); | ||
const buffer = await readFile(path.resolve(root, 'temp', '0002.heic')); | ||
|
||
// quality of 92% | ||
const output92 = await convert({ buffer, format: 'JPEG' }); | ||
await assertImage(output92, 'image/jpeg', control); | ||
|
||
// quality of 100% | ||
const output100 = await convert({ buffer, format: 'JPEG', quality: 1 }); | ||
await assertImage(output100, 'image/jpeg', control); | ||
|
||
expect(output92).to.not.deep.equal(output100); | ||
expect(output92.length).to.be.below(output100.length); | ||
}); | ||
|
||
it('converts known image to png', async () => { | ||
const control = await readControl('0002-control.png'); | ||
const buffer = await readFile(path.resolve(root, 'temp', '0002.heic')); | ||
|
||
const output = await convert({ buffer, format: 'PNG' }); | ||
await assertImage(output, 'image/png', control); | ||
}); | ||
|
||
it('converts multiple images inside a single file to jpeg', async () => { | ||
const controls = await Promise.all([ | ||
readControl('0003-0-control.png'), | ||
readControl('0003-1-control.png'), | ||
]); | ||
const buffer = await readFile(path.resolve(root, 'temp', '0003.heic')); | ||
const images = await convert.all({ buffer, format: 'JPEG' }); | ||
|
||
expect(images).to.have.lengthOf(3); | ||
|
||
for (let { i, control } of [ | ||
{ i: 0, control: controls[0] }, | ||
{ i: 1, control: controls[1] }, | ||
{ i: 2, control: controls[1] }, | ||
]) { | ||
expect(images[i]).to.have.a.property('convert').and.to.be.a('function'); | ||
await assertImage(await images[i].convert(), 'image/jpeg', control); | ||
} | ||
}); | ||
|
||
it('converts multiple images inside a single file to png', async () => { | ||
const controls = await Promise.all([ | ||
readControl('0003-0-control.png'), | ||
readControl('0003-1-control.png'), | ||
]); | ||
const buffer = await readFile(path.resolve(root, 'temp', '0003.heic')); | ||
const images = await convert.all({ buffer, format: 'PNG' }); | ||
|
||
expect(images).to.have.lengthOf(3); | ||
|
||
for (let { i, control } of [ | ||
{ i: 0, control: controls[0] }, | ||
{ i: 1, control: controls[1] }, | ||
{ i: 2, control: controls[1] }, | ||
]) { | ||
expect(images[i]).to.have.a.property('convert').and.to.be.a('function'); | ||
await assertImage(await images[i].convert(), 'image/png', control); | ||
} | ||
}); | ||
|
||
[ | ||
{ when: 'converting only the main image', method: convert }, | ||
{ when: 'converting all images', method: convert.all } | ||
].forEach(({ when, method }) => { | ||
it(`errors for an unknown output format when ${when}`, async () => { | ||
const buffer = Buffer.from(Math.random().toString() + Math.random().toString()); | ||
|
||
let error; | ||
|
||
await method({ buffer, format: 'PINEAPPLES' }).catch(err => { | ||
error = err; | ||
}); | ||
describe('heic-convert (default wasm libheif)', () => { | ||
runTests(require('..')); | ||
}); | ||
|
||
expect(error).to.be.instanceof(Error) | ||
.and.to.have.property('message', 'output format needs to be one of [JPEG,PNG]'); | ||
}); | ||
// I wouldn't say these are strictly required, but 🤷♀️ | ||
describe('heic-convert (legacy js libheif)', () => { | ||
const libheif = require('libheif-js/index.js'); | ||
const decodeLib = require('heic-decode/lib.js'); | ||
const formats = require('../formats-node.js'); | ||
|
||
it(`errors if data other than a HEIC image is passed in when ${when}`, async () => { | ||
const buffer = Buffer.from(Math.random().toString() + Math.random().toString()); | ||
const { one: decodeOne, all: decodeAll } = decodeLib(libheif); | ||
decodeOne.all = decodeAll; | ||
|
||
let error; | ||
const { one, all } = require('../lib.js')(decodeOne, formats); | ||
|
||
await method({ buffer, format: 'JPEG' }).catch(err => { | ||
error = err; | ||
}); | ||
const convert = one; | ||
convert.all = all; | ||
|
||
expect(error).to.be.instanceof(TypeError) | ||
.and.to.have.property('message', 'input buffer is not a HEIC image'); | ||
}); | ||
}); | ||
runTests(convert); | ||
}); |
Oops, something went wrong.