diff --git a/lib/miaf_utils.js b/lib/miaf_utils.js new file mode 100644 index 0000000..47135a9 --- /dev/null +++ b/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' }; +}; diff --git a/lib/parse_stream/avif.js b/lib/parse_stream/avif.js index 16f86a7..930ed3d 100644 --- a/lib/parse_stream/avif.js +++ b/lib/parse_stream/avif.js @@ -1,3 +1,4 @@ + 'use strict'; /* eslint-disable consistent-return */ @@ -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'); @@ -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' }); @@ -115,7 +57,7 @@ function readAvifSize(parser, imgType) { }); } else { safeSkip(parser, size, function () { - readAvifSize(parser, type); + readAvifSize(parser, fileType); }); } }); @@ -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); }); }); diff --git a/lib/parse_sync/avif.js b/lib/parse_sync/avif.js index 6589d73..ab17477 100644 --- a/lib/parse_sync/avif.js +++ b/lib/parse_sync/avif.js @@ -1,9 +1,3 @@ -// 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 */ @@ -12,93 +6,29 @@ 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'); -/* - * 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; + // also limit first box size to 65535 by checking first 2 bytes + // + if (!sliceEq(data, 4, SIG_FTYP) || data[0] !== 0 || data[1] !== 0) return; - var type = String.fromCharCode.apply(null, data.slice(8, 12)); + var firstBox = miaf.readBox(data, 0); + if (!firstBox) return; - if (type !== 'avif' && type !== 'heic') return; + var fileType = miaf.getMimeType(firstBox.data); + if (!fileType) return; var newData, box, offset; - for (offset = 0, newData = null; (box = readBox(data, offset)); offset = box.end) { + for (offset = firstBox.end, newData = null; (box = miaf.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; @@ -111,15 +41,15 @@ module.exports = function (data) { if (!newData) return; data = newData; - var imgSize = readAvifSizeFromMeta(data); + var imgSize = miaf.readSizeFromMeta(data); if (!imgSize) return; return { width: imgSize.width, height: imgSize.height, - type: type === 'heic' ? 'heic' : 'avif', - mime: type === 'heic' ? 'image/heic' : 'image/avif', + type: fileType.type, + mime: fileType.mime, wUnits: 'px', hUnits: 'px' }; diff --git a/test/fixtures/MIAF002.heic b/test/fixtures/MIAF002.heic new file mode 100644 index 0000000..71e4ebe Binary files /dev/null and b/test/fixtures/MIAF002.heic differ diff --git a/test/fixtures/README.md b/test/fixtures/README.md index 1e699d5..9ca09b9 100644 --- a/test/fixtures/README.md +++ b/test/fixtures/README.md @@ -1 +1,5 @@ Fixtures derived from https://github.com/nodejs/node/issues/37, author: @substack, license: public domain/CC0. + +`image4.heic` is from [tigranbs/test-heic-images](https://github.com/tigranbs/test-heic-images). + +`MIAF002.heic` is from [nokiatech/heif_conformance](https://github.com/nokiatech/heif_conformance). diff --git a/test/fixtures/image4.heic b/test/fixtures/image4.heic new file mode 100644 index 0000000..efd119a Binary files /dev/null and b/test/fixtures/image4.heic differ diff --git a/test/test_formats.js b/test/test_formats.js index 13c0a46..081d3fc 100644 --- a/test/test_formats.js +++ b/test/test_formats.js @@ -313,24 +313,25 @@ describe('File formats', function () { }); - it('minimal AVIF file', async function () { - let buf = str2arr(( - '\0\0\0\x0C ftyp avif ' + - '\0\0\0\x30 meta \0\0\0\0 ' + - '\0\0\0\x24 iprp ' + - '\0\0\0\x1C ipco ' + - '\0\0\0\x14 ispe \0\0\0\0 \x01\x02\x03\x04 \x05\x06\x07\x08' - ).replace(/ /g, '')); + it('should detect HEIC - image4.heic', async function () { + let file = path.join(__dirname, 'fixtures', 'image4.heic'); + let size = await probe(fs.createReadStream(file)); - let size = await probe(Readable.from([ Buffer.from(buf) ])); + assert.deepStrictEqual(size, { width: 700, height: 476, type: 'heic', mime: 'image/heic', wUnits: 'px', hUnits: 'px' }); + }); - assert.deepStrictEqual(size, { width: 16909060, height: 84281096, type: 'avif', mime: 'image/avif', wUnits: 'px', hUnits: 'px' }); + + it('should detect HEIC - MIAF002.heic', async function () { + let file = path.join(__dirname, 'fixtures', 'MIAF002.heic'); + let size = await probe(fs.createReadStream(file)); + + assert.deepStrictEqual(size, { width: 160, height: 160, type: 'heif', mime: 'image/heif', wUnits: 'px', hUnits: 'px' }); }); - it('minimal HEIC file', async function () { + it('minimal AVIF file', async function () { let buf = str2arr(( - '\0\0\0\x0C ftyp heic ' + + '\0\0\0\x14 ftyp avif \0\0\0\0 mif1' + '\0\0\0\x30 meta \0\0\0\0 ' + '\0\0\0\x24 iprp ' + '\0\0\0\x1C ipco ' + @@ -339,14 +340,14 @@ describe('File formats', function () { let size = await probe(Readable.from([ Buffer.from(buf) ])); - assert.deepStrictEqual(size, { width: 16909060, height: 84281096, type: 'heic', mime: 'image/heic', wUnits: 'px', hUnits: 'px' }); + assert.deepStrictEqual(size, { width: 16909060, height: 84281096, type: 'avif', mime: 'image/avif', wUnits: 'px', hUnits: 'px' }); }); // these images have real sizes last: https://github.com/tigranbs/test-heic-images it('should keep last size in ispe', async function () { let buf = str2arr(( - '\0\0\0\x0C ftyp avif ' + + '\0\0\0\x14 ftyp avif \0\0\0\0 mif1' + '\0\0\0\x44 meta \0\0\0\0 ' + '\0\0\0\x38 iprp ' + '\0\0\0\x30 ipco ' + @@ -362,7 +363,7 @@ describe('File formats', function () { it('invalid format (mp4)', async function () { let buf = str2arr(( - '\0\0\0\x0C ftyp 3gp5 ' + + '\0\0\0\x10 ftyp 3gp5 \0\0\0\0' + '\0\0\0\x30 meta \0\0\0\0 ' + '\0\0\0\x24 iprp ' + '\0\0\0\x1C ipco ' + @@ -380,7 +381,7 @@ describe('File formats', function () { let buf; buf = str2arr(( - '\0\0\0\x06 ftyp avif ' + + '\0\0\0\x06 ftyp avif \0\0\0\0 mif1' + '\0\0\0\x30 meta \0\0\0\0 ' + '\0\0\0\x24 iprp ' + '\0\0\0\x1C ipco ' + @@ -393,7 +394,7 @@ describe('File formats', function () { ); buf = str2arr(( - '\0\0\0\x0C ftyp avif ' + + '\0\0\0\x14 ftyp avif \0\0\0\0 mif1' + '\0\0\0\x06 meta \0\0\0\0 ' + '\0\0\0\x24 iprp ' + '\0\0\0\x1C ipco ' + @@ -406,7 +407,7 @@ describe('File formats', function () { ); buf = str2arr(( - '\0\0\0\x0C ftyp avif ' + + '\0\0\0\x14 ftyp avif \0\0\0\0 mif1' + '\0\0\0\x30 meta \0\0\0\0 ' + '\0\0\0\x06 iprp ' + '\0\0\0\x1C ipco ' + @@ -419,7 +420,7 @@ describe('File formats', function () { ); buf = str2arr(( - '\0\0\0\x0C ftyp avif ' + + '\0\0\0\x14 ftyp avif \0\0\0\0 mif1' + '\0\0\0\x30 meta \0\0\0\0 ' + '\0\0\0\x24 iprp ' + '\0\0\0\x06 ipco ' + @@ -432,7 +433,7 @@ describe('File formats', function () { ); buf = str2arr(( - '\0\0\0\x0C ftyp avif ' + + '\0\0\0\x14 ftyp avif \0\0\0\0 mif1' + '\0\0\0\x30 meta \0\0\0\0 ' + '\0\0\0\x24 iprp ' + '\0\0\0\x1C ipco ' + @@ -450,7 +451,7 @@ describe('File formats', function () { let buf; buf = str2arr(( - '\0\0\0\x0C ftyp avif ' + + '\0\0\0\x14 ftyp avif \0\0\0\0 mif1' + '\0\0\0\x0C mdat \0\0\0\0 ' + '\0\0\0\x30 meta \0\0\0\0 ' + '\0\0\0\x24 iprp ' + @@ -467,7 +468,8 @@ describe('File formats', function () { it('coverage - other fields', async function () { let buf = str2arr(( - '\0\0\0\x0C ftyp avif ' + + '\0\0\0\x14 ftyp avif \0\0\0\0 mif1' + + '\0\0\0\x08 free ' + '\0\0\0\x0C free \0\0\0\0 ' + '\0\0\0\x4C meta \0\0\0\0 ' + '\0\0\0\x08 free ' + @@ -483,6 +485,76 @@ describe('File formats', function () { assert.deepStrictEqual(size, { width: 16909060, height: 84281096, type: 'avif', mime: 'image/avif', wUnits: 'px', hUnits: 'px' }); }); + + + it('coverage - mime types', async function () { + let buf; + + buf = str2arr(( + '\0\0\0\x14 ftyp hevc \0\0\0\0 msf1' + + '\0\0\0\x30 meta \0\0\0\0 ' + + '\0\0\0\x24 iprp ' + + '\0\0\0\x1C ipco ' + + '\0\0\0\x14 ispe \0\0\0\0 \x01\x02\x03\x04 \x05\x06\x07\x08' + ).replace(/ /g, '')); + + assert.strictEqual( + (await probe(Readable.from([ Buffer.from(buf) ]))).mime, + 'image/heic-sequence' + ); + + buf = str2arr(( + '\0\0\0\x14 ftyp mif1 \0\0\0\0 avif' + + '\0\0\0\x30 meta \0\0\0\0 ' + + '\0\0\0\x24 iprp ' + + '\0\0\0\x1C ipco ' + + '\0\0\0\x14 ispe \0\0\0\0 \x01\x02\x03\x04 \x05\x06\x07\x08' + ).replace(/ /g, '')); + + assert.strictEqual( + (await probe(Readable.from([ Buffer.from(buf) ]))).mime, + 'image/avif' + ); + + buf = str2arr(( + '\0\0\0\x14 ftyp msf1 \0\0\0\0 heix' + + '\0\0\0\x30 meta \0\0\0\0 ' + + '\0\0\0\x24 iprp ' + + '\0\0\0\x1C ipco ' + + '\0\0\0\x14 ispe \0\0\0\0 \x01\x02\x03\x04 \x05\x06\x07\x08' + ).replace(/ /g, '')); + + assert.strictEqual( + (await probe(Readable.from([ Buffer.from(buf) ]))).mime, + 'image/heif-sequence' + ); + + buf = str2arr(( + '\0\0\0\x14 ftyp mif1 \0\0\0\0 heix' + + '\0\0\0\x30 meta \0\0\0\0 ' + + '\0\0\0\x24 iprp ' + + '\0\0\0\x1C ipco ' + + '\0\0\0\x14 ispe \0\0\0\0 \x01\x02\x03\x04 \x05\x06\x07\x08' + ).replace(/ /g, '')); + + assert.strictEqual( + (await probe(Readable.from([ Buffer.from(buf) ]))).mime, + 'image/heif' + ); + + buf = str2arr(( + '\0\0\0\x14 ftyp mif1 \0\0\0\0 xxxx' + + '\0\0\0\x30 meta \0\0\0\0 ' + + '\0\0\0\x24 iprp ' + + '\0\0\0\x1C ipco ' + + '\0\0\0\x14 ispe \0\0\0\0 \x01\x02\x03\x04 \x05\x06\x07\x08' + ).replace(/ /g, '')); + + assert.strictEqual( + (await probe(Readable.from([ Buffer.from(buf) ]))).mime, + 'image/avif' + ); + }); }); @@ -495,36 +567,39 @@ describe('File formats', function () { }); - it('minimal AVIF file', function () { - let buf = str2arr(( - '\0\0\0\x0C ftyp avif ' + - '\0\0\0\x30 meta \0\0\0\0 ' + - '\0\0\0\x24 iprp ' + - '\0\0\0\x1C ipco ' + - '\0\0\0\x14 ispe \0\0\0\0 \x01\x02\x03\x04 \x05\x06\x07\x08' - ).replace(/ /g, '')); + it('should detect HEIC - image4.heic', async function () { + let file = path.join(__dirname, 'fixtures', 'image4.heic'); + let size = probe.sync(fs.readFileSync(file)); - assert.deepStrictEqual(probe.sync(buf), { width: 16909060, height: 84281096, type: 'avif', mime: 'image/avif', wUnits: 'px', hUnits: 'px' }); + assert.deepStrictEqual(size, { width: 700, height: 476, type: 'heic', mime: 'image/heic', wUnits: 'px', hUnits: 'px' }); }); - it('minimal HEIC file', function () { + it('should detect HEIC - MIAF002.heic', async function () { + let file = path.join(__dirname, 'fixtures', 'MIAF002.heic'); + let size = probe.sync(fs.readFileSync(file)); + + assert.deepStrictEqual(size, { width: 160, height: 160, type: 'heif', mime: 'image/heif', wUnits: 'px', hUnits: 'px' }); + }); + + + it('minimal AVIF file', function () { let buf = str2arr(( - '\0\0\0\x0C ftyp heic ' + + '\0\0\0\x14 ftyp avif \0\0\0\0 mif1' + '\0\0\0\x30 meta \0\0\0\0 ' + '\0\0\0\x24 iprp ' + '\0\0\0\x1C ipco ' + '\0\0\0\x14 ispe \0\0\0\0 \x01\x02\x03\x04 \x05\x06\x07\x08' ).replace(/ /g, '')); - assert.deepStrictEqual(probe.sync(buf), { width: 16909060, height: 84281096, type: 'heic', mime: 'image/heic', wUnits: 'px', hUnits: 'px' }); + assert.deepStrictEqual(probe.sync(buf), { width: 16909060, height: 84281096, type: 'avif', mime: 'image/avif', wUnits: 'px', hUnits: 'px' }); }); // these images have real sizes last: https://github.com/tigranbs/test-heic-images it('should keep last size in ispe', function () { let buf = str2arr(( - '\0\0\0\x0C ftyp avif ' + + '\0\0\0\x14 ftyp avif \0\0\0\0 mif1' + '\0\0\0\x44 meta \0\0\0\0 ' + '\0\0\0\x38 iprp ' + '\0\0\0\x30 ipco ' + @@ -538,7 +613,7 @@ describe('File formats', function () { it('invalid format (mp4)', function () { let buf = str2arr(( - '\0\0\0\x0C ftyp 3gp5 ' + + '\0\0\0\x10 ftyp 3gp5 \0\0\0\0' + '\0\0\0\x30 meta \0\0\0\0 ' + '\0\0\0\x24 iprp ' + '\0\0\0\x1C ipco ' + @@ -553,7 +628,7 @@ describe('File formats', function () { let buf; buf = str2arr(( - '\0\0\0\x06 ftyp avif ' + + '\0\0\0\x06 ftyp avif \0\0\0\0 mif1' + '\0\0\0\x30 meta \0\0\0\0 ' + '\0\0\0\x24 iprp ' + '\0\0\0\x1C ipco ' + @@ -563,7 +638,7 @@ describe('File formats', function () { assert.strictEqual(probe.sync(buf), null); buf = str2arr(( - '\0\0\0\x0C ftyp avif ' + + '\0\0\0\x14 ftyp avif \0\0\0\0 mif1' + '\0\0\0\x06 meta \0\0\0\0 ' + '\0\0\0\x24 iprp ' + '\0\0\0\x1C ipco ' + @@ -573,7 +648,7 @@ describe('File formats', function () { assert.strictEqual(probe.sync(buf), null); buf = str2arr(( - '\0\0\0\x0C ftyp avif ' + + '\0\0\0\x14 ftyp avif \0\0\0\0 mif1' + '\0\0\0\x30 meta \0\0\0\0 ' + '\0\0\0\x06 iprp ' + '\0\0\0\x1C ipco ' + @@ -583,7 +658,7 @@ describe('File formats', function () { assert.strictEqual(probe.sync(buf), null); buf = str2arr(( - '\0\0\0\x0C ftyp avif ' + + '\0\0\0\x14 ftyp avif \0\0\0\0 mif1' + '\0\0\0\x30 meta \0\0\0\0 ' + '\0\0\0\x24 iprp ' + '\0\0\0\x06 ipco ' + @@ -593,7 +668,7 @@ describe('File formats', function () { assert.strictEqual(probe.sync(buf), null); buf = str2arr(( - '\0\0\0\x0C ftyp avif ' + + '\0\0\0\x14 ftyp avif \0\0\0\0 mif1' + '\0\0\0\x30 meta \0\0\0\0 ' + '\0\0\0\x24 iprp ' + '\0\0\0\x1C ipco ' + @@ -608,7 +683,7 @@ describe('File formats', function () { let buf; buf = str2arr(( - '\0\0\0\x0C ftyp avif ' + + '\0\0\0\x14 ftyp avif \0\0\0\0 mif1' + '\0\0\0\x0C mdat \0\0\0\0 ' + '\0\0\0\x30 meta \0\0\0\0 ' + '\0\0\0\x24 iprp ' + @@ -622,7 +697,8 @@ describe('File formats', function () { it('coverage - other fields', function () { let buf = str2arr(( - '\0\0\0\x0C ftyp avif ' + + '\0\0\0\x14 ftyp avif \0\0\0\0 mif1' + + '\0\0\0\x08 free ' + '\0\0\0\x0C free \0\0\0\0 ' + '\0\0\0\x4C meta \0\0\0\0 ' + '\0\0\0\x08 free ' + @@ -636,6 +712,76 @@ describe('File formats', function () { assert.deepStrictEqual(probe.sync(buf), { width: 16909060, height: 84281096, type: 'avif', mime: 'image/avif', wUnits: 'px', hUnits: 'px' }); }); + + + it('coverage - mime types', async function () { + let buf; + + buf = str2arr(( + '\0\0\0\x14 ftyp hevc \0\0\0\0 msf1' + + '\0\0\0\x30 meta \0\0\0\0 ' + + '\0\0\0\x24 iprp ' + + '\0\0\0\x1C ipco ' + + '\0\0\0\x14 ispe \0\0\0\0 \x01\x02\x03\x04 \x05\x06\x07\x08' + ).replace(/ /g, '')); + + assert.strictEqual( + probe.sync(buf).mime, + 'image/heic-sequence' + ); + + buf = str2arr(( + '\0\0\0\x14 ftyp mif1 \0\0\0\0 avif' + + '\0\0\0\x30 meta \0\0\0\0 ' + + '\0\0\0\x24 iprp ' + + '\0\0\0\x1C ipco ' + + '\0\0\0\x14 ispe \0\0\0\0 \x01\x02\x03\x04 \x05\x06\x07\x08' + ).replace(/ /g, '')); + + assert.strictEqual( + probe.sync(buf).mime, + 'image/avif' + ); + + buf = str2arr(( + '\0\0\0\x14 ftyp msf1 \0\0\0\0 heix' + + '\0\0\0\x30 meta \0\0\0\0 ' + + '\0\0\0\x24 iprp ' + + '\0\0\0\x1C ipco ' + + '\0\0\0\x14 ispe \0\0\0\0 \x01\x02\x03\x04 \x05\x06\x07\x08' + ).replace(/ /g, '')); + + assert.strictEqual( + probe.sync(buf).mime, + 'image/heif-sequence' + ); + + buf = str2arr(( + '\0\0\0\x14 ftyp mif1 \0\0\0\0 heix' + + '\0\0\0\x30 meta \0\0\0\0 ' + + '\0\0\0\x24 iprp ' + + '\0\0\0\x1C ipco ' + + '\0\0\0\x14 ispe \0\0\0\0 \x01\x02\x03\x04 \x05\x06\x07\x08' + ).replace(/ /g, '')); + + assert.strictEqual( + probe.sync(buf).mime, + 'image/heif' + ); + + buf = str2arr(( + '\0\0\0\x14 ftyp mif1 \0\0\0\0 xxxx' + + '\0\0\0\x30 meta \0\0\0\0 ' + + '\0\0\0\x24 iprp ' + + '\0\0\0\x1C ipco ' + + '\0\0\0\x14 ispe \0\0\0\0 \x01\x02\x03\x04 \x05\x06\x07\x08' + ).replace(/ /g, '')); + + assert.strictEqual( + probe.sync(buf).mime, + 'image/avif' + ); + }); });