/** * ExifReader * http://github.com/mattiasw/exifreader * Copyright (C) 2011-2026 Mattias Wallander * 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/. */ /* global Buffer */ import {objectAssign, decompress, withDecompressBudget, COMPRESSION_METHOD_BROTLI, getDataView, getByteString, getStringValueFromArray, assertPromiseSupport, setProperty} from './utils.js'; import {isFilePathOrURL, isBrowserFileObject, loadFile, loadFileObject} from './file-loaders.js'; import {makeLoadAuto, validateAutoOptions} from './load-auto.js'; import Constants from './constants.js'; import {getCalculatedGpsValue} from './tag-names-utils.js'; import ByteOrder from './byte-order.js'; import {getTiffHeaderOffset} from './image-header-iso-bmff.js'; import ImageHeader from './image-header.js'; import Tags from './tags.js'; import {getValueBudget, addDecompressedValueAllowance, takeEmbeddedExifThumbnail} from './tags-helpers.js'; import MpfTags from './mpf-tags.js'; import FileTags from './file-tags.js'; import JxlFileTags from './jxl-file-tags.js'; import IsoBmffFileTags from './iso-bmff-file-tags.js'; import JfifTags from './jfif-tags.js'; import IptcTags from './iptc-tags.js'; import XmpTags from './xmp-tags.js'; import PhotoshopTags from './photoshop-tags.js'; import IccTags from './icc-tags.js'; import CanonTags from './canon-tags.js'; import PentaxTags from './pentax-tags.js'; import PngFileTags from './png-file-tags.js'; import PngTextTags from './png-text-tags.js'; import PngTags from './png-tags.js'; import Vp8xTags from './vp8x-tags.js'; import GifFileTags from './gif-file-tags.js'; import Thumbnail from './thumbnail.js'; import Composite from './composite.js'; import {createTagFilter} from './tag-filter.js'; import {buildTagsFromMergeSteps, isThenable, EXIF_CARRIED_GROUP_ORDER} from './loadview-pipeline.js'; import exifErrors from './errors.js'; // Brob XMP is bounded by the file size, as brob Exif is, plus room for one JPEG // standard XMP segment so that a small file can still carry a typical packet. const BROB_XMP_SIZE_PER_INPUT_SIZE = 4; const BROB_XMP_SIZE_ALLOWANCE = 64 * 1024; // An embedded thumbnail is sliced from a decoded or decompressed view that can be // far larger than the file. A real JPEG thumbnail shrinks only a little when // compressed, so bound the embedded thumbnails of a file by twice its size in // total, plus room for a typical thumbnail. const EMBEDDED_THUMBNAIL_SIZE_PER_INPUT_SIZE = 2; const EMBEDDED_THUMBNAIL_SIZE_ALLOWANCE = 64 * 1024; export default { load, loadView, errors: exifErrors, }; export const errors = exifErrors; export function load(data, options = {}) { if (options.length === 'auto') { validateAutoOptions(options); return makeLoadAuto(loadFromData)(data, options); } if (isFilePathOrURL(data) || isBrowserFileObject(data)) { assertPromiseSupport(); const loadFileFunction = isFilePathOrURL(data) ? loadFile : loadFileObject; const asyncOptions = objectAssign({}, options, {async: true}); return loadFileFunction(data, asyncOptions).then((fileContents) => loadFromData(fileContents, asyncOptions)); } return loadFromData(data, options); } function loadFromData(data, options) { if (isNodeBuffer(data)) { // File data read in Node can share the underlying buffer with other // data. Therefore it's safest to get a new one to avoid weird bugs. data = (new Uint8Array(data)).buffer; } return loadView(getDataView(data), options); } function isNodeBuffer(data) { try { return Buffer.isBuffer(data); } catch (error) { return false; } } export function loadView( dataView, { expanded = false, async = false, computed = false, includeUnknown = false, includeOffsets = false, domParser = undefined, includeTags = undefined, excludeTags = undefined, decompress: decompressConfig = undefined } = {} ) { dataView = getSelfContainedDataView(dataView); decompressConfig = withDecompressBudget(decompressConfig); const tagFilter = createTagFilter({includeTags, excludeTags}); const parsedGroups = Object.create(null); const mergeSteps = []; const deferredResults = Object.create(null); const deferredPromises = []; let pngTextIsAsync = false; let thumbnailIfdTags = undefined; let valueBudget = undefined; let embeddedXmpStepForFlat = undefined; let decompressedApplicationNotesSize = 0; const { fileType, fileDataOffset, jfifDataOffset, tiffHeaderOffset, iptcDataOffset, xmpChunks, iccChunks, mpfDataOffset, pngHeaderOffset, pngTextChunks, pngChunkOffsets, vp8xChunkOffset, gifHeaderOffset, brobExifChunk, brobXmpChunk, jxlCodestreamOffset, imageDimensionsOffset, pixelInformationOffset, metadataBlocks, metadataTruncated, exifDataView, xmpDataView } = ImageHeader.parseAppMarkers(dataView, async, expanded && includeOffsets); const fileHasMetaData = hasPotentialMetaData({ fileType, fileDataOffset, jfifDataOffset, tiffHeaderOffset, iptcDataOffset, xmpChunks, iccChunks, mpfDataOffset, pngHeaderOffset, pngTextChunks, pngChunkOffsets, vp8xChunkOffset, gifHeaderOffset, jxlCodestreamOffset, }); if ( Constants.USE_JPEG && Constants.USE_FILE && fileDataOffset !== undefined && tagFilter.shouldParseGroup('file') && shouldReadFileTagsForFileTypeOnlyOptimization(includeTags) ) { const readTags = FileTags.read(dataView, fileDataOffset); const parsedFileTags = filterTagsForParse('file', readTags, tagFilter); parsedGroups.file = parsedFileTags; if (tagFilter.shouldReturnGroup('file')) { mergeSteps.push({ type: 'mergeGroupAssign', groupKey: 'file', parsedTags: parsedFileTags, }); } } if ( Constants.USE_JPEG && Constants.USE_JFIF && jfifDataOffset !== undefined && tagFilter.shouldParseGroup('jfif') ) { const readTags = JfifTags.read(dataView, jfifDataOffset); const parsedJfifTags = filterTagsForParse('jfif', readTags, tagFilter); parsedGroups.jfif = parsedJfifTags; if (tagFilter.shouldReturnGroup('jfif')) { mergeSteps.push({ type: 'mergeGroupAssign', groupKey: 'jfif', parsedTags: parsedJfifTags, }); } } if ( Constants.USE_EXIF && tiffHeaderOffset !== undefined && tagFilter.shouldParseGroup('exif') ) { const {tags: readTags, byteOrder, valueBudget: exifValueBudget} = readExifTagsSafely( exifDataView || dataView, tiffHeaderOffset, includeUnknown, computed, tagFilter ); valueBudget = exifValueBudget; if (readTags.Thumbnail) { thumbnailIfdTags = readTags.Thumbnail; delete readTags.Thumbnail; } const parsedExifTags = filterTagsForParse('exif', readTags, tagFilter); parsedGroups.exif = parsedExifTags; const carriedGroups = readExifCarriedGroups( parsedExifTags, exifDataView || dataView, tiffHeaderOffset, byteOrder, valueBudget, {decompressed: false, hasIptcBlock: iptcDataOffset !== undefined} ); for (let i = 0; i < EXIF_CARRIED_GROUP_ORDER.length; i++) { const groupKey = EXIF_CARRIED_GROUP_ORDER[i]; if (carriedGroups[groupKey] === undefined) { continue; } parsedGroups[groupKey] = carriedGroups[groupKey]; if (!tagFilter.shouldReturnGroup(groupKey)) { continue; } if (groupKey === 'xmp') { const step = { type: 'mergeXmpGroupAssign', parsedTags: carriedGroups.xmp, }; if (expanded) { mergeSteps.push(step); } else { embeddedXmpStepForFlat = step; } } else { mergeSteps.push({ type: 'mergeGroupAssign', groupKey, parsedTags: carriedGroups[groupKey], }); } } if (tagFilter.shouldReturnGroup('exif')) { mergeSteps.push({ type: 'mergeGroupAssign', groupKey: 'exif', parsedTags: parsedExifTags, }); } if (!expanded && embeddedXmpStepForFlat) { mergeSteps.push(embeddedXmpStepForFlat); embeddedXmpStepForFlat = undefined; } } if (valueBudget === undefined && dataView !== undefined) { valueBudget = getValueBudget(dataView); } if ( Constants.USE_JPEG && Constants.USE_IPTC && iptcDataOffset !== undefined && tagFilter.shouldParseGroup('iptc') ) { const readTags = IptcTags.read(dataView, iptcDataOffset, includeUnknown, tagFilter, valueBudget); const parsedIptcTags = filterTagsForParse('iptc', readTags, tagFilter); parsedGroups.iptc = parsedIptcTags; if (tagFilter.shouldReturnGroup('iptc')) { mergeSteps.push({ type: 'mergeGroupAssign', groupKey: 'iptc', parsedTags: parsedIptcTags, }); } } if ( Constants.USE_XMP && hasXmpData(xmpChunks) && tagFilter.shouldParseGroup('xmp') ) { const readTags = XmpTags.read(xmpDataView || dataView, xmpChunks, domParser); const parsedXmpTags = filterTagsForParse('xmp', readTags, tagFilter); parsedGroups.xmp = parsedXmpTags; if (tagFilter.shouldReturnGroup('xmp')) { mergeSteps.push({ type: 'mergeXmpGroupAssign', parsedTags: parsedXmpTags, }); } } const readEmbeddedExifThumbnail = getEmbeddedExifThumbnailReader(tagFilter, dataView); if ( Constants.USE_JXL && Constants.USE_EXIF && brobExifChunk && tiffHeaderOffset === undefined && tagFilter.shouldParseGroup('exif') && async ) { const compressedExifData = getBrobDataView(dataView, brobExifChunk); deferredPromises.push( decompress(compressedExifData, COMPRESSION_METHOD_BROTLI, undefined, 'dataview', decompressConfig) .then((decompressedDataView) => { const brobTiffHeaderOffset = getTiffHeaderOffset(decompressedDataView, 0); addDecompressedValueAllowance(valueBudget, decompressedDataView.byteLength); const {tags: readTags, byteOrder: brobByteOrder} = Tags.read( decompressedDataView, brobTiffHeaderOffset, includeUnknown, computed, tagFilter, valueBudget ); const thumbnail = takeEmbeddedExifThumbnail( readTags, decompressedDataView, brobTiffHeaderOffset, readEmbeddedExifThumbnail ); const carriedGroups = readEmbeddedExifCarriedGroups( decompressedDataView, readTags, brobTiffHeaderOffset, brobByteOrder, valueBudget, {decompressed: true, hasIptcBlock: false} ); deferredResults.brobExif = {exifTags: readTags, carriedGroups, thumbnail}; }) .catch(() => { deferredResults.brobExif = undefined; }) ); mergeSteps.push({ type: 'mergeBrobExifDeferred', deferredKey: 'brobExif', }); } if ( Constants.USE_JXL && Constants.USE_XMP && brobXmpChunk && !hasXmpData(xmpChunks) && tagFilter.shouldParseGroup('xmp') && async ) { const compressedXmpData = getBrobDataView(dataView, brobXmpChunk); deferredPromises.push( decompress(compressedXmpData, COMPRESSION_METHOD_BROTLI, undefined, 'dataview', decompressConfig) .then((decompressedDataView) => { if (exceedsBrobXmpSizeBound(decompressedDataView.byteLength, dataView)) { deferredResults.brobXmp = {}; return; } deferredResults.brobXmp = XmpTags.read( decompressedDataView, [{dataOffset: 0, length: decompressedDataView.byteLength}], domParser ); }) .catch(() => { deferredResults.brobXmp = {}; }) ); mergeSteps.push({ type: 'mergeBrobXmpDeferred', deferredKey: 'brobXmp', }); } if ( Constants.USE_ICC && hasIccData(iccChunks) && tagFilter.shouldParseGroup('icc') ) { const readTags = IccTags.read(dataView, iccChunks, async, decompressConfig); if (isThenable(readTags)) { if (!async) { throw new Error('Promise is required when async mode is enabled.'); } deferredPromises.push(readTags.then((resolvedTags) => { deferredResults.iccApp = resolvedTags; })); mergeSteps.push({ type: 'mergeIccDeferred', deferredKey: 'iccApp', }); } else { const parsedIccTags = filterTagsForParse('icc', readTags, tagFilter); parsedGroups.icc = parsedIccTags; if (tagFilter.shouldReturnGroup('icc')) { mergeSteps.push({ type: 'mergeGroupAssign', groupKey: 'icc', parsedTags: parsedIccTags, }); } } } if ( Constants.USE_MPF && mpfDataOffset !== undefined && tagFilter.shouldParseGroup('mpf') ) { const readMpfTags = MpfTags.read( dataView, mpfDataOffset, includeUnknown, computed, tagFilter, valueBudget ); const parsedMpfTags = filterTagsForParse('mpf', readMpfTags, tagFilter); parsedGroups.mpf = parsedMpfTags; if (tagFilter.shouldReturnGroup('mpf')) { mergeSteps.push({ type: 'mergeGroupAssign', groupKey: 'mpf', parsedTags: parsedMpfTags, }); } } if ( Constants.USE_PNG && Constants.USE_PNG_FILE && pngHeaderOffset !== undefined && tagFilter.shouldParseGroup('png') ) { const readTags = PngFileTags.read(dataView, pngHeaderOffset); const parsedPngFileTags = filterTagsForParse('png', readTags, tagFilter); parsedGroups.pngFile = parsedPngFileTags; if (tagFilter.shouldReturnGroup('png')) { mergeSteps.push({ type: 'mergePngFile', parsedTags: parsedPngFileTags, }); } } if ( Constants.USE_PNG && hasPngTextData(pngTextChunks) && ( tagFilter.shouldParseGroup('png') || tagFilter.shouldParseGroup('exif') || tagFilter.shouldParseGroup('iptc') ) ) { const {readTags, embeddedExifTags, embeddedIptcTags, embeddedExifThumbnail, exifCarriedGroups, readTagsPromise} = PngTextTags.read( dataView, pngTextChunks, async, includeUnknown, computed, tagFilter, decompressConfig, valueBudget, readEmbeddedExifThumbnail, readEmbeddedExifCarriedGroups ); pngTextIsAsync = !!readTagsPromise; mergeSteps.push({ type: 'processPngTextReadTags', readTags, embeddedExifTags, embeddedIptcTags, embeddedExifThumbnail, exifCarriedGroups, }); if (readTagsPromise) { deferredPromises.push(readTagsPromise.then((tagList) => { deferredResults.pngTextTagList = tagList; })); mergeSteps.push({ type: 'processPngTextReadTagsDeferredList', deferredKey: 'pngTextTagList', }); } } if ( Constants.USE_PNG && pngChunkOffsets !== undefined && tagFilter.shouldParseGroup('png') ) { const readTags = PngTags.read(dataView, pngChunkOffsets); const parsedPngChunkTags = filterTagsForParse('png', readTags, tagFilter); parsedGroups.pngChunk = parsedPngChunkTags; if (tagFilter.shouldReturnGroup('png')) { mergeSteps.push({ type: 'mergePngChunk', parsedTags: parsedPngChunkTags, }); } } if ( Constants.USE_WEBP && vp8xChunkOffset !== undefined && tagFilter.shouldParseGroup('riff') ) { const readTags = Vp8xTags.read(dataView, vp8xChunkOffset); const parsedRiffTags = filterTagsForParse('riff', readTags, tagFilter); parsedGroups.riff = parsedRiffTags; if (tagFilter.shouldReturnGroup('riff')) { mergeSteps.push({ type: 'mergeGroupMerge', groupKey: 'riff', parsedTags: parsedRiffTags, }); } } if ( Constants.USE_GIF && gifHeaderOffset !== undefined && tagFilter.shouldParseGroup('gif') ) { const readTags = GifFileTags.read(dataView, gifHeaderOffset); const parsedGifTags = filterTagsForParse('gif', readTags, tagFilter); parsedGroups.gif = parsedGifTags; if (tagFilter.shouldReturnGroup('gif')) { mergeSteps.push({ type: 'mergeGroupMerge', groupKey: 'gif', parsedTags: parsedGifTags, }); } } if ( Constants.USE_JXL && jxlCodestreamOffset !== undefined && tagFilter.shouldParseGroup('file') ) { const readTags = JxlFileTags.read(dataView, jxlCodestreamOffset); const parsedJxlFileTags = filterTagsForParse('file', readTags, tagFilter); parsedGroups.file = parsedJxlFileTags; if (tagFilter.shouldReturnGroup('file')) { mergeSteps.push({ type: 'mergeGroupAssign', groupKey: 'file', parsedTags: parsedJxlFileTags, }); } } if ( (Constants.USE_HEIC || Constants.USE_AVIF) && (imageDimensionsOffset !== undefined || pixelInformationOffset !== undefined) && tagFilter.shouldParseGroup('file') ) { const readTags = IsoBmffFileTags.read(dataView, imageDimensionsOffset, pixelInformationOffset); const parsedIsoBmffFileTags = filterTagsForParse('file', readTags, tagFilter); parsedGroups.file = parsedIsoBmffFileTags; if (tagFilter.shouldReturnGroup('file')) { mergeSteps.push({ type: 'mergeGroupAssign', groupKey: 'file', parsedTags: parsedIsoBmffFileTags, }); } } if (Constants.USE_EXIF) { mergeSteps.push({type: 'gps'}); } if (Constants.USE_EXIF || Constants.USE_XMP) { mergeSteps.push({type: 'composite'}); } mergeSteps.push({type: 'thumbnail'}); mergeSteps.push({type: 'fileType'}); if (expanded && includeOffsets) { mergeSteps.push({ type: 'metadataRange', metadataBlocks, metadataTruncated: !!metadataTruncated, }); } if (!fileHasMetaData) { throw new exifErrors.MetadataMissingError(); } const pipelineDependencies = { objectAssign, hasPngTextData, filterTagsForParse, filterTagsForReturn, // A plain property here would keep the function alive for the bundler, // so a build without Exif would still carry getGpsGroupFromExifTags. getGpsGroupFromExifTags: Constants.USE_EXIF ? getGpsGroupFromExifTags : undefined, // A plain property here would keep the import alive for the bundler, so // a build with neither Exif nor XMP would still carry composite.js. Composite: (Constants.USE_EXIF || Constants.USE_XMP) ? Composite : undefined, Thumbnail, }; if (async) { assertPromiseSupport(); return Promise.all(deferredPromises).then(() => { const tags = buildTagsFromMergeSteps({ mergeSteps, deferredResults, parsedGroups, expanded, tagFilter, dataView, tiffHeaderOffset, exifDataView, fileType, pngTextChunks, pngTextIsAsync, thumbnailIfdTags, deps: pipelineDependencies, }); return tags; }); } return buildTagsFromMergeSteps({ mergeSteps, deferredResults, parsedGroups, expanded, tagFilter, dataView, tiffHeaderOffset, exifDataView, fileType, pngTextChunks, pngTextIsAsync, thumbnailIfdTags, deps: pipelineDependencies, }); function shouldReadFileTagsForFileTypeOnlyOptimization(includeTagsOptions) { if (!includeTagsOptions) { return true; } const {composite, file} = includeTagsOptions; if (composite === true || (Array.isArray(composite) && composite.length > 0)) { return true; } return !(Array.isArray(file) && file.length === 1 && file[0] === 'FileType'); } // Drops MakerNote.__offset once the maker note has been read, even when a // reader throws. function readExifCarriedGroups(parsedExifTags, exifTiffDataView, exifTiffHeaderOffset, exifByteOrder, exifValueBudget, source) { const groups = {}; try { if ( Constants.USE_IPTC && parsedExifTags['IPTC-NAA'] && !source.hasIptcBlock && tagFilter.shouldParseGroup('iptc') ) { const readIptcTags = IptcTags.read( parsedExifTags['IPTC-NAA'].value, 0, includeUnknown, tagFilter, exifValueBudget ); groups.iptc = filterTagsForParse('iptc', readIptcTags, tagFilter); } if ( Constants.USE_XMP && parsedExifTags['ApplicationNotes'] && isNumberArray(parsedExifTags['ApplicationNotes'].value) && !hasXmpData(xmpChunks) && tagFilter.shouldParseGroup('xmp') && (!source.decompressed || takeDecompressedApplicationNotesAllowance(parsedExifTags['ApplicationNotes'].value.length)) ) { const readXmpTags = XmpTags.read( getByteString(parsedExifTags['ApplicationNotes'].value), undefined, domParser ); groups.xmp = filterTagsForParse('xmp', readXmpTags, tagFilter); } if ( Constants.USE_PHOTOSHOP && parsedExifTags['ImageSourceData'] && parsedExifTags['PhotoshopSettings'] && Array.isArray(parsedExifTags['PhotoshopSettings'].value) && tagFilter.shouldParseGroup('photoshop') ) { const readPhotoshopTags = PhotoshopTags.read( parsedExifTags['PhotoshopSettings'].value, includeUnknown, tagFilter ); groups.photoshop = filterTagsForParse('photoshop', readPhotoshopTags, tagFilter); } if ( Constants.USE_ICC && parsedExifTags['ICC_Profile'] && !hasIccData(iccChunks) && tagFilter.shouldParseGroup('icc') ) { const readIccTags = IccTags.read( parsedExifTags['ICC_Profile'].value, [{ offset: 0, length: parsedExifTags['ICC_Profile'].value.length, chunkNumber: 1, chunksTotal: 1 }] ); groups.icc = filterTagsForParse('icc', readIccTags, tagFilter); } if ( Constants.USE_MAKER_NOTES && parsedExifTags['MakerNote'] && tagFilter.shouldParseGroup('makerNotes') ) { if (hasCanonData(parsedExifTags)) { groups.makerNotes = CanonTags.read( exifTiffDataView, exifTiffHeaderOffset, parsedExifTags['MakerNote'].__offset, exifByteOrder, includeUnknown, computed, tagFilter, exifValueBudget ); } else if (hasPentaxType1Data(parsedExifTags)) { groups.makerNotes = PentaxTags.read( exifTiffDataView, exifTiffHeaderOffset, parsedExifTags['MakerNote'].__offset, includeUnknown, computed, tagFilter, exifValueBudget ); } } } finally { if (parsedExifTags['MakerNote']) { delete parsedExifTags['MakerNote'].__offset; } } return groups; } // The brob XMP bound covers the decompressed Exif blocks of a file together, // so that several cannot each pass it. function takeDecompressedApplicationNotesAllowance(byteLength) { if (exceedsBrobXmpSizeBound(decompressedApplicationNotesSize + byteLength, dataView)) { return false; } decompressedApplicationNotesSize += byteLength; return true; } // A maker note that throws must drop the groups, not the Exif tags already read. function readEmbeddedExifCarriedGroups(exifTiffDataView, readTags, exifTiffHeaderOffset, exifByteOrder, exifValueBudget, source) { try { return readExifCarriedGroups( filterTagsForParse('exif', readTags, tagFilter), exifTiffDataView, exifTiffHeaderOffset, exifByteOrder, exifValueBudget, source ); } catch (error) { return {}; } } } // A caller-supplied DataView can be a window into a larger buffer. Extractors // slice dataView.buffer, so copy the window out to keep them inside the image. function getSelfContainedDataView(dataView) { if (isWindowIntoLargerBuffer(dataView)) { return getDataView(dataView.buffer.slice(dataView.byteOffset, dataView.byteOffset + dataView.byteLength)); } return dataView; } function isWindowIntoLargerBuffer(dataView) { return isDataViewLike(dataView) && ((dataView.byteOffset > 0) || (dataView.byteLength < dataView.buffer.byteLength)); } // A DataView from another realm, an iframe for example, fails instanceof, so // match on the shape instead. A typed array has no getUint8 and the Node Buffer // wrapper has no byteOffset, so neither is treated as a window here. function isDataViewLike(dataView) { return !!dataView && (typeof dataView.getUint8 === 'function') && (typeof dataView.byteOffset === 'number') && (typeof dataView.byteLength === 'number') && !!dataView.buffer && (typeof dataView.buffer.slice === 'function'); } function getBrobDataView(dataView, brobChunk) { // The box length is untrusted, so bound the view by the bytes present. The // JXL header parser never reports a dataOffset past the end of the view. const length = Math.max(0, Math.min(brobChunk.length, dataView.byteLength - brobChunk.dataOffset)); if (isDataViewLike(dataView)) { return new DataView(dataView.buffer, dataView.byteOffset + brobChunk.dataOffset, length); } // The Node Buffer wrapper has no ArrayBuffer to view, so copy the bytes out. const bytes = new Uint8Array(length); for (let i = 0; i < length; i++) { bytes[i] = dataView.getUint8(brobChunk.dataOffset + i); } return new DataView(bytes.buffer); } function exceedsBrobXmpSizeBound(byteLength, inputDataView) { return byteLength > BROB_XMP_SIZE_PER_INPUT_SIZE * inputDataView.byteLength + BROB_XMP_SIZE_ALLOWANCE; } function readExifTagsSafely(dataView, tiffHeaderOffset, includeUnknown, computed, tagFilter) { // Created outside the try so values decoded before a throw still count // against the budget that the maker note and MPF reads draw from. The data // view is only missing in unit tests that fake the tag readers. const valueBudget = dataView === undefined ? undefined : getValueBudget(dataView); try { const {tags, byteOrder} = Tags.read(dataView, tiffHeaderOffset, includeUnknown, computed, tagFilter, valueBudget); return {tags, byteOrder, valueBudget}; } catch (error) { // A malformed TIFF header (an out-of-bounds offset or an invalid byte // order marker, e.g. from a HEIC/AVIF iloc that does not point at real // Exif) must not abort the whole parse. Skip Exif instead, but keep the // same {tags, byteOrder, valueBudget} shape so callers can destructure // safely. return {tags: {}, byteOrder: ByteOrder.BIG_ENDIAN, valueBudget}; } } function getEmbeddedExifThumbnailReader(tagFilter, inputDataView) { if ( !Constants.USE_EXIF || !Constants.USE_THUMBNAIL || !tagFilter.shouldReturnTag('thumbnail', 'Thumbnail') ) { return undefined; } let copiedThumbnailSize = 0; return (dataView, thumbnailIfdTags, tiffHeaderOffset) => { const parsedThumbnailIfdTags = filterTagsForParse('thumbnail', thumbnailIfdTags, tagFilter); const lengthTag = parsedThumbnailIfdTags.JPEGInterchangeFormatLength; const sizeBound = EMBEDDED_THUMBNAIL_SIZE_PER_INPUT_SIZE * inputDataView.byteLength + EMBEDDED_THUMBNAIL_SIZE_ALLOWANCE; if (!hasLengthWithin(lengthTag, sizeBound - copiedThumbnailSize)) { return parsedThumbnailIfdTags; } const thumbnail = Thumbnail.get(dataView, parsedThumbnailIfdTags, tiffHeaderOffset); if (thumbnail.image) { copiedThumbnailSize += lengthTag.value; } return thumbnail; }; } function hasLengthWithin(lengthTag, maxLength) { return !!lengthTag && typeof lengthTag.value === 'number' && lengthTag.value <= maxLength; } function filterTagsForParse(groupKey, readTags, tagFilter) { if (!tagFilter.isActive) { return readTags; } return filterTags(groupKey, readTags, tagFilter.shouldParseTag); } function filterTagsForReturn(groupKey, readTags, tagFilter) { if (!tagFilter.isActive) { return readTags; } return filterTags(groupKey, readTags, tagFilter.shouldReturnTag); } function filterTags(groupKey, readTags, matchesTag) { if (!readTags) { return readTags; } const filteredTags = {}; for (const tagName in readTags) { const tagValue = readTags[tagName]; const tagId = getTagId(tagValue); if (matchesTag(groupKey, tagName, tagId)) { setProperty(filteredTags, tagName, tagValue); } } return filteredTags; function getTagId(tagValue) { if (!tagValue) { return undefined; } if (Array.isArray(tagValue)) { if (tagValue.length === 0) { return undefined; } return tagValue[0].id; } return tagValue.id; } } function hasPotentialMetaData({ fileType, fileDataOffset, jfifDataOffset, tiffHeaderOffset, iptcDataOffset, xmpChunks, iccChunks, mpfDataOffset, pngHeaderOffset, pngTextChunks, pngChunkOffsets, vp8xChunkOffset, gifHeaderOffset, jxlCodestreamOffset, imageDimensionsOffset, pixelInformationOffset, }) { return !!fileType || ( Constants.USE_JPEG && Constants.USE_FILE && fileDataOffset !== undefined ) || ( Constants.USE_JPEG && Constants.USE_JFIF && jfifDataOffset !== undefined ) || ( Constants.USE_EXIF && tiffHeaderOffset !== undefined ) || ( Constants.USE_JPEG && Constants.USE_IPTC && iptcDataOffset !== undefined ) || ( Constants.USE_XMP && hasXmpData(xmpChunks) ) || ( Constants.USE_ICC && hasIccData(iccChunks) ) || ( Constants.USE_MPF && mpfDataOffset !== undefined ) || ( Constants.USE_PNG && Constants.USE_PNG_FILE && pngHeaderOffset !== undefined ) || ( Constants.USE_PNG && hasPngTextData(pngTextChunks) ) || ( Constants.USE_PNG && pngChunkOffsets !== undefined ) || ( Constants.USE_WEBP && vp8xChunkOffset !== undefined ) || ( Constants.USE_GIF && gifHeaderOffset !== undefined ) || ( Constants.USE_JXL && jxlCodestreamOffset !== undefined ) || ( (Constants.USE_HEIC || Constants.USE_AVIF) && (imageDimensionsOffset !== undefined || pixelInformationOffset !== undefined) ); } function getGpsGroupFromExifTags(exifTags) { let gps = undefined; if (exifTags.GPSLatitude && exifTags.GPSLatitudeRef) { gps = gps || {}; try { gps.Latitude = getCalculatedGpsValue(exifTags.GPSLatitude.value); if (exifTags.GPSLatitudeRef.value.join('') === 'S') { gps.Latitude = -gps.Latitude; } } catch (error) { // Ignore. } } if (exifTags.GPSLongitude && exifTags.GPSLongitudeRef) { gps = gps || {}; try { gps.Longitude = getCalculatedGpsValue(exifTags.GPSLongitude.value); if (exifTags.GPSLongitudeRef.value.join('') === 'W') { gps.Longitude = -gps.Longitude; } } catch (error) { // Ignore. } } if (exifTags.GPSAltitude && exifTags.GPSAltitudeRef) { gps = gps || {}; try { gps.Altitude = exifTags.GPSAltitude.value[0] / exifTags.GPSAltitude.value[1]; if (exifTags.GPSAltitudeRef.value === 1) { gps.Altitude = -gps.Altitude; } } catch (error) { // Ignore. } } if (!gps) { return undefined; } return gps; } // ASCII values hold strings and holes, values of two or more rationals hold // number pairs: neither converts to the character codes of an XMP packet. function isNumberArray(value) { return Array.isArray(value) && typeof value[0] === 'number'; } function hasXmpData(xmpChunks) { return Array.isArray(xmpChunks) && xmpChunks.length > 0; } function hasIccData(iccDataOffsets) { return Array.isArray(iccDataOffsets) && iccDataOffsets.length > 0; } function hasCanonData(tags) { return tags['Make'] && tags['Make'].value && Array.isArray(tags['Make'].value) && tags['Make'].value[0] === 'Canon' && tags['MakerNote'] && tags['MakerNote'].__offset; } function hasPentaxType1Data(tags) { const PENTAX_ID_STRING = 'PENTAX '; return Array.isArray(tags['MakerNote'].value) && tags['MakerNote'].value.length > PENTAX_ID_STRING.length && getStringValueFromArray(tags['MakerNote'].value.slice(0, PENTAX_ID_STRING.length)) === PENTAX_ID_STRING && tags['MakerNote'].__offset; } function hasPngTextData(pngTextChunks) { return Array.isArray(pngTextChunks) && pngTextChunks.length > 0; }