// Utils used to parse miaf-based files (avif/heic/heif) // // 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 */ /* eslint-disable no-bitwise */ var readUInt16BE = require('./common').readUInt16BE; 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 unbox(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 { boxtype: String.fromCharCode.apply(null, data.slice(offset + 4, offset + 8)), data: data.slice(offset + 8, offset + size), end: offset + size }; } module.exports.unbox = unbox; // parses `meta` -> `iprp` -> `ipco` box, returns: // { // sizes: [ { width, height } ], // transforms: [ { type, value } ] // } function scan_ipco(data, sandbox) { var offset = 0; for (;;) { var box = unbox(data, offset); if (!box) break; switch (box.boxtype) { case 'ispe': sandbox.sizes.push({ width: readUInt32BE(box.data, 4), height: readUInt32BE(box.data, 8) }); break; case 'irot': sandbox.transforms.push({ type: 'irot', value: box.data[0] & 3 }); break; case 'imir': sandbox.transforms.push({ type: 'imir', value: box.data[0] & 1 }); break; } offset = box.end; } } function readUIntBE(data, offset, size) { var result = 0; for (var i = 0; i < size; i++) { result = result * 256 + (data[offset + i] || 0); } return result; } // parses `meta` -> `iloc` box function scan_iloc(data, sandbox) { var offset_size = (data[4] >> 4) & 0xF; var length_size = data[4] & 0xF; var base_offset_size = (data[5] >> 4) & 0xF; var item_count = readUInt16BE(data, 6); var offset = 8; for (var i = 0; i < item_count; i++) { var item_ID = readUInt16BE(data, offset); offset += 2; var data_reference_index = readUInt16BE(data, offset); offset += 2; var base_offset = readUIntBE(data, offset, base_offset_size); offset += base_offset_size; var extent_count = readUInt16BE(data, offset); offset += 2; if (data_reference_index === 0 && extent_count === 1) { var first_extent_offset = readUIntBE(data, offset, offset_size); var first_extent_length = readUIntBE(data, offset + offset_size, length_size); sandbox.item_loc[item_ID] = { length: first_extent_length, offset: first_extent_offset + base_offset }; } offset += extent_count * (offset_size + length_size); } } // parses `meta` -> `iinf` box function scan_iinf(data, sandbox) { var item_count = readUInt16BE(data, 4); var offset = 6; for (var i = 0; i < item_count; i++) { var box = unbox(data, offset); if (!box) break; if (box.boxtype === 'infe') { var item_id = readUInt16BE(box.data, 4); var item_name = ''; for (var pos = 8; pos < box.data.length && box.data[pos]; pos++) { item_name += String.fromCharCode(box.data[pos]); } sandbox.item_inf[item_name] = item_id; } offset = box.end; } } // parses `meta` -> `iprp` box function scan_iprp(data, sandbox) { var offset = 0; for (;;) { var box = unbox(data, offset); if (!box) break; if (box.boxtype === 'ipco') scan_ipco(box.data, sandbox); offset = box.end; } } // parses `meta` box function scan_meta(data, sandbox) { var offset = 4; // version + flags for (;;) { var box = unbox(data, offset); if (!box) break; if (box.boxtype === 'iprp') scan_iprp(box.data, sandbox); if (box.boxtype === 'iloc') scan_iloc(box.data, sandbox); if (box.boxtype === 'iinf') scan_iinf(box.data, sandbox); offset = box.end; } } // get image with largest single dimension as base function getMaxSize(sizes) { var maxWidthSize = sizes.reduce(function (a, b) { return a.width > b.width || (a.width === b.width && a.height > b.height) ? a : b; }); var maxHeightSize = sizes.reduce(function (a, b) { return a.height > b.height || (a.height === b.height && a.width > b.width) ? a : b; }); var maxSize; if (maxWidthSize.width > maxHeightSize.height || (maxWidthSize.width === maxHeightSize.height && maxWidthSize.height > maxHeightSize.width)) { maxSize = maxWidthSize; } else { maxSize = maxHeightSize; } return maxSize; } module.exports.readSizeFromMeta = function (data) { var sandbox = { sizes: [], transforms: [], item_inf: {}, item_loc: {} }; scan_meta(data, sandbox); if (!sandbox.sizes.length) return; var maxSize = getMaxSize(sandbox.sizes); var orientation = 1; // convert imir/irot to exif orientation sandbox.transforms.forEach(function (transform) { var rotate_ccw = { 1: 6, 2: 5, 3: 8, 4: 7, 5: 4, 6: 3, 7: 2, 8: 1 }; var mirror_vert = { 1: 4, 2: 3, 3: 2, 4: 1, 5: 6, 6: 5, 7: 8, 8: 7 }; if (transform.type === 'imir') { if (transform.value === 0) { // vertical flip orientation = mirror_vert[orientation]; } else { // horizontal flip = vertical flip + 180 deg rotation orientation = mirror_vert[orientation]; orientation = rotate_ccw[orientation]; orientation = rotate_ccw[orientation]; } } if (transform.type === 'irot') { // counter-clockwise rotation 90 deg 0-3 times for (var i = 0; i < transform.value; i++) { orientation = rotate_ccw[orientation]; } } }); var exif_location = null; if (sandbox.item_inf.Exif) { exif_location = sandbox.item_loc[sandbox.item_inf.Exif]; } return { width: maxSize.width, height: maxSize.height, orientation: sandbox.transforms.length ? orientation : null, variants: sandbox.sizes, exif_location: exif_location }; }; module.exports.getMimeType = function (data) { var brand = String.fromCharCode.apply(null, data.slice(0, 4)); var compat = {}; compat[brand] = true; for (var i = 8; i < data.length; i += 4) { compat[String.fromCharCode.apply(null, data.slice(i, i + 4))] = 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' }; };