astro-ghostcms/.pnpm-store/v3/files/56/5f581aaec088b25f47c6fdfaf82...

302 lines
7.6 KiB
Plaintext

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