Skip to content

Commit

Permalink
Add support for AVIF file format
Browse files Browse the repository at this point in the history
  • Loading branch information
rlidwka committed Feb 26, 2021
1 parent dde12f9 commit 62b47ca
Show file tree
Hide file tree
Showing 7 changed files with 297 additions and 2 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ rules:
max-depth: [ 1, 6 ]
max-nested-callbacks: [ 1, 4 ]
# string can exceed 80 chars, but should not overflow github website :)
max-len: [ 2, 120, 1000 ]
max-len: [ 2, 120, 1000, { ignoreUrls: true } ]
new-cap: 2
new-parens: 2
# Postponed
Expand Down
147 changes: 147 additions & 0 deletions lib/parse_stream/avif.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
'use strict';

/* eslint-disable consistent-return */


var ParserStream = require('../common').ParserStream;
var str2arr = require('../common').str2arr;
var sliceEq = require('../common').sliceEq;
var readUInt32BE = require('../common').readUInt32BE;

var SIG_FTYP = str2arr('ftyp');


function readBox(data, offset) {
if (data.length < 4 + offset) return null;

var size = readUInt32BE(data, offset);

// size includes first 4 bytes (length)
if (data.length < size + offset || size < 8) return null;

// if size === 1, real size is following uint64 (only for big boxes, not needed)
// if size === 0, real size is until the end of the file (only for big boxes, not needed)

return {
type: String.fromCharCode.apply(null, data.slice(offset + 4, offset + 8)),
data: data.slice(offset + 8, offset + size),
end: offset + size
};
}


function readAvifSizeFromMeta(data) {
var newData, box, offset;

for (offset = 4 /* version+flags */, newData = null; (box = readBox(data, offset)); offset = box.end) {
if (box.type === 'iprp') {
newData = box.data;
break;
}
}

if (!newData) return;
data = newData;

for (offset = 0, newData = null; (box = readBox(data, offset)); offset = box.end) {
if (box.type === 'ipco') {
newData = box.data;
break;
}
}

if (!newData) return;
data = newData;

for (offset = 0, newData = null; (box = readBox(data, offset)); offset = box.end) {
if (box.type === 'ispe') {
newData = box.data;
// no break here (!), second ispe may override the first, see
// https://github.com/tigranbs/test-heic-images
}
}

if (!newData) return;
data = newData;

return {
width: readUInt32BE(data, 4),
height: readUInt32BE(data, 8)
};
}


function readAvifSize(parser, imgType) {
parser._bytes(8, function (data) {
var size = readUInt32BE(data, 0) - 8;
var type = String.fromCharCode.apply(null, data.slice(4, 8));

if (size <= 0) {
parser._skipBytes(Infinity);
parser.push(null);
return;
} else if (type === 'mdat') {
parser._skipBytes(Infinity);
parser.push(null);
return;
} else if (type === 'meta') {
parser._bytes(size, function (data) {
parser._skipBytes(Infinity);

var imgSize = readAvifSizeFromMeta(data);

if (!imgSize) return;

parser.push({
width: imgSize.width,
height: imgSize.height,
type: imgType === 'heic' ? 'heic' : 'avif',
mime: imgType === 'heic' ? 'image/heic' : 'image/avif',
wUnits: 'px',
hUnits: 'px'
});

parser.push(null);
});
} else {
parser._skipBytes(size, function () {
readAvifSize(parser, type);
});
}
});
}


module.exports = function () {
var parser = new ParserStream();

parser._bytes(12, function (data) {
if (!sliceEq(data, 4, SIG_FTYP)) {
parser._skipBytes(Infinity);
parser.push(null);
return;
}

var type = String.fromCharCode.apply(null, data.slice(8, 12));

if (type !== 'heic' && type !== 'avif') {
parser._skipBytes(Infinity);
parser.push(null);
return;
}

var size = readUInt32BE(data, 0) - 12;

if (size <= 0) {
parser._skipBytes(Infinity);
parser.push(null);
return;
}

parser._skipBytes(size, function () {
readAvifSize(parser, type);
});
});

return parser;
};
126 changes: 126 additions & 0 deletions lib/parse_sync/avif.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// ISO media file spec:
// https://web.archive.org/web/20180219054429/http://l.web.umkc.edu/lizhu/teaching/2016sp.video-communication/ref/mp4.pdf
//
// ISO image file format spec:
// https://standards.iso.org/ittf/PubliclyAvailableStandards/c066067_ISO_IEC_23008-12_2017.zip
//

/* eslint-disable consistent-return */

'use strict';


var str2arr = require('../common').str2arr;
var sliceEq = require('../common').sliceEq;
var readUInt32BE = require('../common').readUInt32BE;

var SIG_FTYP = str2arr('ftyp');


/*
* interface Box {
* size: uint32; // if size == 0, box lasts until EOF
* boxtype: char[4];
* largesize?: uint64; // only if size == 1
* usertype?: char[16]; // only if boxtype == 'uuid'
* }
*/
function readBox(data, offset) {
if (data.length < 4 + offset) return null;

var size = readUInt32BE(data, offset);

// size includes first 4 bytes (length)
if (data.length < size + offset || size < 8) return null;

// if size === 1, real size is following uint64 (only for big boxes, not needed)
// if size === 0, real size is until the end of the file (only for big boxes, not needed)

return {
type: String.fromCharCode.apply(null, data.slice(offset + 4, offset + 8)),
data: data.slice(offset + 8, offset + size),
end: offset + size
};
}


function readAvifSizeFromMeta(data) {
var newData, box, offset;

for (offset = 4 /* version+flags */, newData = null; (box = readBox(data, offset)); offset = box.end) {
if (box.type === 'iprp') {
newData = box.data;
break;
}
}

if (!newData) return;
data = newData;

for (offset = 0, newData = null; (box = readBox(data, offset)); offset = box.end) {
if (box.type === 'ipco') {
newData = box.data;
break;
}
}

if (!newData) return;
data = newData;

for (offset = 0, newData = null; (box = readBox(data, offset)); offset = box.end) {
if (box.type === 'ispe') {
newData = box.data;
// no break here (!), second ispe may override the first, see
// https://github.com/tigranbs/test-heic-images
}
}

if (!newData) return;
data = newData;

return {
width: readUInt32BE(data, 4),
height: readUInt32BE(data, 8)
};
}


module.exports = function (data) {
// ISO media file (avif format) starts with ftyp box:
// 0000 0020 6674 7970 6176 6966
// (length) f t y p a v i f
//
if (!sliceEq(data, 4, SIG_FTYP)) return;

var type = String.fromCharCode.apply(null, data.slice(8, 12));

if (type !== 'avif' && type !== 'heic') return;

var newData, box, offset;

for (offset = 0, newData = null; (box = readBox(data, offset)); offset = box.end) {
// mdat block SHOULD be last (but not strictly required),
// so it's unlikely that metadata is after it
if (box.type === 'mdat') return;
if (box.type === 'meta') {
newData = box.data;
break;
}
}

if (!newData) return;
data = newData;

var imgSize = readAvifSizeFromMeta(data);

if (!imgSize) return;

return {
width: imgSize.width,
height: imgSize.height,
type: type === 'heic' ? 'heic' : 'avif',
mime: type === 'heic' ? 'image/heic' : 'image/avif',
wUnits: 'px',
hUnits: 'px'
};
};
1 change: 1 addition & 0 deletions lib/parsers_stream.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

module.exports = {
avif: require('./parse_stream/avif'),
bmp: require('./parse_stream/bmp'),
gif: require('./parse_stream/gif'),
ico: require('./parse_stream/ico'),
Expand Down
3 changes: 2 additions & 1 deletion lib/parsers_sync.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@


module.exports = {
avif: require('./parse_sync/avif'),
bmp: require('./parse_sync/bmp'),
gif: require('./parse_sync/gif'),
jpeg: require('./parse_sync/jpeg'),
ico: require('./parse_sync/ico'),
jpeg: require('./parse_sync/jpeg'),
png: require('./parse_sync/png'),
psd: require('./parse_sync/psd'),
svg: require('./parse_sync/svg'),
Expand Down
Binary file added test/fixtures/iojs_logo.avif
Binary file not shown.
20 changes: 20 additions & 0 deletions test/test_formats.js
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,26 @@ describe('File formats', function () {
});


describe('AVIF', function () {
it('should detect AVIF', async function () {
let file = path.join(__dirname, 'fixtures', 'iojs_logo.avif');
let size = await probe(fs.createReadStream(file));

assert.deepStrictEqual(size, { width: 367, height: 187, type: 'avif', mime: 'image/avif', wUnits: 'px', hUnits: 'px' });
});
});


describe('AVIF (sync)', function () {
it('should detect AVIF', function () {
let file = path.join(__dirname, 'fixtures', 'iojs_logo.avif');
let size = probe.sync(fs.readFileSync(file));

assert.deepStrictEqual(size, { width: 367, height: 187, type: 'avif', mime: 'image/avif', wUnits: 'px', hUnits: 'px' });
});
});


describe('PSD', function () {
it('should detect PSD', async function () {
let file = path.join(__dirname, 'fixtures', 'empty.psd');
Expand Down

0 comments on commit 62b47ca

Please sign in to comment.