Skip to content

Commit

Permalink
Add support for AVIF files
Browse files Browse the repository at this point in the history
  • Loading branch information
mattiasw committed Apr 6, 2024
1 parent fbeddc8 commit 9497a58
Show file tree
Hide file tree
Showing 62 changed files with 1,238 additions and 381 deletions.
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@ You can try it out on the
| File type | Exif | IPTC | XMP | ICC | MPF | Photoshop | Thumbnail | Image details |
| ----------|---------|---------|---------|---------|---------|---------------|-----------|---------------|
| JPEG | **yes** | **yes** | **yes** | **yes** | **yes** | **some*** | **yes** | **yes** |
| TIFF | **yes** | **yes** | **yes** | **yes** | ??? | ??? | no | no |
| TIFF | **yes** | **yes** | **yes** | **yes** | ??? | ??? | N/A | N/A |
| PNG | **yes** | **yes** | **yes** | **yes** | ??? | ??? | no | **yes** |
| HEIC/HEIF | **yes** | no | no | **yes** | ??? | ??? | no | no |
| HEIC/HEIF | **yes** | no | **yes** | **yes** | ??? | ??? | no | no |
| AVIF | **yes** | no | **yes** | **yes** | ??? | ??? | no | no |
| WebP | **yes** | no | **yes** | **yes** | ??? | ??? | **yes** | **yes** |
| GIF | no | no | no | no | no | no | no | **yes** |
| GIF | N/A | N/A | N/A | N/A | N/A | N/A | N/A | **yes** |

- `Image details` = image width, height, etc. read from image header.
- `N/A` = The feature is not applicable to this file type.
- `???` = may be supported in any file type using Exif but it has only been tested
on JPEGs.
- `*` = A draft implementation of Photoshop tags have been added with
Expand Down
2 changes: 1 addition & 1 deletion dist/exif-reader.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/exif-reader.js.map

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,10 @@
},
"nyc": {
"check-coverage": true,
"statements": 95,
"branches": 89,
"statements": 93,
"branches": 88,
"functions": 98,
"lines": 95,
"lines": 93,
"reporter": [
"lcov",
"text"
Expand Down
1 change: 1 addition & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export default {
USE_JPEG: true,
USE_PNG: true,
USE_HEIC: true,
USE_AVIF: true,
USE_WEBP: true,
USE_GIF: true
};
1 change: 1 addition & 0 deletions src/exif-reader.js
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,7 @@ export function loadView(
} else {
tags.FileType = fileType;
}
foundMetaData = true;
}

if (!foundMetaData) {
Expand Down
42 changes: 42 additions & 0 deletions src/image-header-avif.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */

// Specification:
// https://aomediacodec.github.io/av1-avif

import {parseBox, findOffsets} from './image-header-iso-bmff.js';

export default {
isAvifFile,
findAvifOffsets
};

/**
* Checks if the provided data view represents an AVIF file.
*
* @param {DataView} dataView - The data view to check.
* @returns {boolean} True if the data view represents an AVIF file, false otherwise.
*/
function isAvifFile(dataView) {
if (!dataView) {
return false;
}

try {
const headerBox = parseBox(dataView, 0);
return headerBox && headerBox.majorBrand === 'avif';
} catch (error) {
return false;
}
}

/**
* Finds the offsets of an AVIF file in the provided data view.
*
* @param {DataView} dataView - The data view to find offsets in.
* @returns {Object} An object containing the offsets of the TIFF header, XMP chunks, ICC chunks, and a boolean indicating if any of these exist.
*/
function findAvifOffsets(dataView) {
return findOffsets(dataView);
}
187 changes: 20 additions & 167 deletions src/image-header-heic.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,187 +2,40 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */

import {getStringFromDataView} from './utils.js';
import Constants from './constants.js';
import {parseBox, findOffsets} from './image-header-iso-bmff.js';

export default {
isHeicFile,
findHeicOffsets
};

/**
* Checks if the provided data view represents a HEIC/HEIF file.
*
* @param {DataView} dataView - The data view to check.
* @returns {boolean} True if the data view represents a HEIC/HEIF file, false otherwise.
*/
function isHeicFile(dataView) {
if (!dataView) {
return false;
}

const HEIC_ID = 'ftyp';
const HEIC_ID_OFFSET = 4;
const HEIC_MAJOR_BRANDS = ['heic', 'heix', 'hevc', 'hevx', 'heim', 'heis', 'hevm', 'hevs', 'mif1'];
const HEIC_MAJOR_BRAND_LENGTH = 4;

const heicMajorBrand = getStringFromDataView(dataView, HEIC_ID_OFFSET + HEIC_ID.length, HEIC_MAJOR_BRAND_LENGTH);

return (getStringFromDataView(dataView, HEIC_ID_OFFSET, HEIC_ID.length) === HEIC_ID)
&& (HEIC_MAJOR_BRANDS.indexOf(heicMajorBrand) !== -1);
}

function findHeicOffsets(dataView) {
if (Constants.USE_EXIF || Constants.USE_ICC) {
const {offset: metaOffset, length: metaLength} = findMetaBox(dataView);
if (metaOffset === undefined) {
return {hasAppMarkers: false};
}

const metaEndOffset = Math.min(metaOffset + metaLength, dataView.byteLength);
const {exifItemOffset, ilocOffset, colrOffset} = findMetaItems(dataView, metaOffset, metaEndOffset);

const exifOffset = findExifOffset(dataView, exifItemOffset, ilocOffset, metaEndOffset);
const iccChunks = findIccChunks(dataView, colrOffset, metaEndOffset);
return {
hasAppMarkers: (exifOffset !== undefined) || (iccChunks !== undefined),
tiffHeaderOffset: exifOffset,
iccChunks
};
}

return {hasAppMarkers: false};
}

function findMetaBox(dataView) {
const BOX_LENGTH_SIZE = 4;
const BOX_TYPE_SIZE = 4;
const BOX_MIN_LENGTH = 8;
const BOX_TYPE_OFFSET = 4;

let offset = 0;

while (offset + BOX_LENGTH_SIZE + BOX_TYPE_SIZE <= dataView.byteLength) {
const boxLength = getBoxLength(dataView, offset);
if (boxLength >= BOX_MIN_LENGTH) {
const boxType = getStringFromDataView(dataView, offset + BOX_TYPE_OFFSET, BOX_TYPE_SIZE);
if (boxType === 'meta') {
return {
offset,
length: boxLength
};
}
}

offset += boxLength;
}

return {
offset: undefined,
length: 0
};
}

function getBoxLength(dataView, offset) {
const BOX_EXTENDED_SIZE_LOW_OFFSET = 12;

const boxLength = dataView.getUint32(offset);
if (extendsToEndOfFile(boxLength)) {
return dataView.byteLength - offset;
}
if (hasExtendedSize(boxLength)) {
if (hasEmptyHighBits(dataView, offset)) {
// It's a bit tricky to handle 64 bit numbers in JavaScript. Let's
// wait until there are real-world examples where it is necessary.
return dataView.getUint32(offset + BOX_EXTENDED_SIZE_LOW_OFFSET);
}
}

return boxLength;
}

function extendsToEndOfFile(boxLength) {
return boxLength === 0;
}

function hasExtendedSize(boxLength) {
return boxLength === 1;
}

function hasEmptyHighBits(dataView, offset) {
const BOX_EXTENDED_SIZE_OFFSET = 8;
return dataView.getUint32(offset + BOX_EXTENDED_SIZE_OFFSET) === 0;
}

function findMetaItems(dataView, offset, metaEndOffset) {
const STRING_SIZE = 4;
const ITEM_INDEX_REL_OFFSET = -4;
const offsets = {
ilocOffset: undefined,
exifItemOffset: undefined,
colrOffset: undefined
};

while ((offset + STRING_SIZE <= metaEndOffset)
&& (!offsets.ilocOffset || !offsets.exifItemOffset || !offsets.colrOffset)) {
const itemName = getStringFromDataView(dataView, offset, STRING_SIZE);
if (Constants.USE_EXIF && (itemName === 'iloc')) {
offsets.ilocOffset = offset;
} else if (Constants.USE_EXIF && (itemName === 'Exif')) {
offsets.exifItemOffset = offset + ITEM_INDEX_REL_OFFSET;
} else if (Constants.USE_ICC && (itemName === 'colr')) {
offsets.colrOffset = offset + ITEM_INDEX_REL_OFFSET;
}

offset++;
}

return offsets;
}

function findExifOffset(dataView, exifItemOffset, offset, metaEndOffset) {
const EXIF_ITEM_OFFSET_SIZE = 2;
const ILOC_DATA_OFFSET = 12;
const EXIF_POINTER_OFFSET = 8;
const EXIF_POINTER_SIZE = 4;
const EXIF_PREFIX_LENGTH_OFFSET = 4;
const ILOC_ITEM_SIZE = 16;

if (!offset || !exifItemOffset || (exifItemOffset + EXIF_ITEM_OFFSET_SIZE > metaEndOffset)) {
return undefined;
}

const exifItemIndex = dataView.getUint16(exifItemOffset);
offset += ILOC_DATA_OFFSET;

while (offset + ILOC_ITEM_SIZE <= metaEndOffset) {
const itemIndex = dataView.getUint16(offset);
if (itemIndex === exifItemIndex) {
const exifPointer = dataView.getUint32(offset + EXIF_POINTER_OFFSET);
if (exifPointer + EXIF_POINTER_SIZE <= dataView.byteLength) {
const exifOffset = dataView.getUint32(exifPointer);
const prefixLength = exifOffset + EXIF_PREFIX_LENGTH_OFFSET;
return exifPointer + prefixLength;
}
}
offset += ILOC_ITEM_SIZE;
try {
const headerBox = parseBox(dataView, 0);
return headerBox && HEIC_MAJOR_BRANDS.indexOf(headerBox.majorBrand) !== -1;
} catch (error) {
return false;
}

return undefined;
}

function findIccChunks(dataView, offset, metaEndOffset) {
const ITEM_TYPE_OFFSET = 8;
const ITEM_TYPE_SIZE = 4;
const ITEM_CONTENT_OFFSET = 12;

if (!offset || (offset + ITEM_CONTENT_OFFSET > metaEndOffset)) {
return undefined;
}

const colorType = getStringFromDataView(dataView, offset + ITEM_TYPE_OFFSET, ITEM_TYPE_SIZE);
if ((colorType !== 'prof') && (colorType !== 'rICC')) {
return undefined;
}

return [{
offset: offset + ITEM_CONTENT_OFFSET,
length: getBoxLength(dataView, offset) - ITEM_CONTENT_OFFSET,
chunkNumber: 1,
chunksTotal: 1
}];
/**
* Finds the offsets of a HEIC file in the provided data view.
*
* @param {DataView} dataView - The data view to find offsets in.
* @returns {Object} An object containing the offsets of the TIFF header, XMP chunks, ICC chunks, and a boolean indicating if any of these exist.
*/
function findHeicOffsets(dataView) {
return findOffsets(dataView);
}
Loading

0 comments on commit 9497a58

Please sign in to comment.