Skip to content

Commit

Permalink
add tests for heic format
Browse files Browse the repository at this point in the history
  • Loading branch information
rlidwka committed Mar 2, 2021
1 parent 7830f59 commit 4251641
Show file tree
Hide file tree
Showing 7 changed files with 358 additions and 205 deletions.
130 changes: 130 additions & 0 deletions lib/miaf_utils.js
@@ -0,0 +1,130 @@
// Utils used to parse miaf-based files (avif/heic/heif)
//
// We look for last `ispe` box (first box is presumably a thumbnail). Images with metadata encoded
// after image data are not supported. Image sequences sometimes can't be recognized if they don't have `ispe` box.
//
// 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
//

'use strict';

/* eslint-disable consistent-return */

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

/*
* 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
};
}


module.exports.readBox = readBox;


module.exports.readSizeFromMeta = function (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.getMimeType = function (data) {
var str = String.fromCharCode.apply(null, data);

var brand = str.slice(0, 4);
var compat = {};

compat[brand] = true;
(str.slice(8).match(/..../g) || []).forEach(function (b) {
compat[b] = true;
});

// heic and avif are superset of miaf, so they should all list mif1 as compatible
if (!compat.mif1 && !compat.msf1 && !compat.miaf) return;

if (brand === 'avif' || brand === 'avis' || brand === 'avio') {
// `.avifs` and `image/avif-sequence` are removed from spec, all files have single type
return { type: 'avif', mime: 'image/avif' };
}

// https://nokiatech.github.io/heif/technical.html
if (brand === 'heic' || brand === 'heix') {
return { type: 'heic', mime: 'image/heic' };
}

if (brand === 'hevc' || brand === 'hevx') {
return { type: 'heic', mime: 'image/heic-sequence' };
}

if (compat.avif || compat.avis) {
return { type: 'avif', mime: 'image/avif' };
}

if (compat.heic || compat.heix || compat.hevc || compat.hevx || compat.heis) {
if (compat.msf1) {
return { type: 'heif', mime: 'image/heif-sequence' };
}
return { type: 'heif', mime: 'image/heif' };
}

return { type: 'avif', mime: 'image/avif' };
};
103 changes: 23 additions & 80 deletions lib/parse_stream/avif.js
@@ -1,3 +1,4 @@

'use strict';

/* eslint-disable consistent-return */
Expand All @@ -7,6 +8,7 @@ var ParserStream = require('../common').ParserStream;
var str2arr = require('../common').str2arr;
var sliceEq = require('../common').sliceEq;
var readUInt32BE = require('../common').readUInt32BE;
var miaf = require('../miaf_utils');

var SIG_FTYP = str2arr('ftyp');

Expand All @@ -21,92 +23,32 @@ function safeSkip(parser, count, callback) {
}


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) {
function readAvifSize(parser, fileType) {
parser._bytes(8, function (data) {
var size = readUInt32BE(data, 0) - 8;
var type = String.fromCharCode.apply(null, data.slice(4, 8));

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

var imgSize = readAvifSizeFromMeta(data);
var imgSize = miaf.readSizeFromMeta(data);

if (!imgSize) return;

parser.push({
width: imgSize.width,
height: imgSize.height,
type: imgType === 'heic' ? 'heic' : 'avif',
mime: imgType === 'heic' ? 'image/heic' : 'image/avif',
type: fileType.type,
mime: fileType.mime,
wUnits: 'px',
hUnits: 'px'
});
Expand All @@ -115,7 +57,7 @@ function readAvifSize(parser, imgType) {
});
} else {
safeSkip(parser, size, function () {
readAvifSize(parser, type);
readAvifSize(parser, fileType);
});
}
});
Expand All @@ -125,31 +67,32 @@ function readAvifSize(parser, imgType) {
module.exports = function () {
var parser = new ParserStream();

parser._bytes(12, function (data) {
if (!sliceEq(data, 4, SIG_FTYP)) {
parser._bytes(8, function (data) {
// limit first box size to 65535 by checking first 2 bytes
if (!sliceEq(data, 4, SIG_FTYP) || data[0] !== 0 || data[1] !== 0) {
parser._skipBytes(Infinity);
parser.push(null);
return;
}

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

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

var size = readUInt32BE(data, 0) - 12;
parser._bytes(size, function (data) {
var fileType = miaf.getMimeType(data);

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

safeSkip(parser, size, function () {
readAvifSize(parser, type);
readAvifSize(parser, fileType);
});
});

Expand Down

0 comments on commit 4251641

Please sign in to comment.