1/* 2 * Copyright (c) 2024 - 2025 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16import type { ImageInfo, ImageData } from '../../../model/Interfaces'; 17import { readUInt, readUInt16BE, toHexString } from '../BytesUtils'; 18 19const EXIF_MARKER = '45786966'; 20const APP1_DATA_SIZE_BYTES = 2; 21const EXIF_HEADER_BYTES = 6; 22const TIFF_BYTE_ALIGN_BYTES = 2; 23const BIG_ENDIAN_BYTE_ALIGN = '4d4d'; 24const LITTLE_ENDIAN_BYTE_ALIGN = '4949'; 25const IDF_ENTRY_BYTES = 12; 26const NUM_DIRECTORY_ENTRIES_BYTES = 2; 27 28function isEXIF(input: Uint8Array): boolean { 29 return toHexString(input, 2, 6) === EXIF_MARKER; 30} 31 32function extractSize(input: Uint8Array, index: number): ImageInfo { 33 return { 34 height: readUInt16BE(input, index), 35 width: readUInt16BE(input, index + 2), 36 }; 37} 38 39 40function extractOrientation(exifBlock: Uint8Array, isBigEndian: boolean): number | undefined { 41 const idfOffset = 8; 42 const offset = EXIF_HEADER_BYTES + idfOffset; 43 const idfDirectoryEntries = readUInt(exifBlock, 16, offset, isBigEndian); 44 for (let directoryEntryNumber = 0; directoryEntryNumber < idfDirectoryEntries; directoryEntryNumber++) { 45 const start = offset + NUM_DIRECTORY_ENTRIES_BYTES + directoryEntryNumber * IDF_ENTRY_BYTES; 46 const end = start + IDF_ENTRY_BYTES; 47 if (start > exifBlock.length) { 48 return undefined; 49 } 50 const block = exifBlock.slice(start, end); 51 const tagNumber = readUInt(block, 16, 0, isBigEndian); 52 if (tagNumber === 274) { 53 const dataFormat = readUInt(block, 16, 2, isBigEndian); 54 if (dataFormat !== 3) { 55 return undefined; 56 } 57 const numberOfComponents = readUInt(block, 32, 4, isBigEndian); 58 if (numberOfComponents !== 1) { 59 return undefined; 60 } 61 return readUInt(block, 16, 8, isBigEndian); 62 } 63 } 64 return undefined; 65} 66 67function validateExifBlock(input: Uint8Array, index: number): number | undefined { 68 const exifBlock = input.slice(APP1_DATA_SIZE_BYTES, index); 69 const byteAlign = toHexString(exifBlock, EXIF_HEADER_BYTES, EXIF_HEADER_BYTES + TIFF_BYTE_ALIGN_BYTES); 70 const isBigEndian = byteAlign === BIG_ENDIAN_BYTE_ALIGN; 71 const isLittleEndian = byteAlign === LITTLE_ENDIAN_BYTE_ALIGN; 72 if (isBigEndian || isLittleEndian) { 73 return extractOrientation(exifBlock, isBigEndian); 74 } 75 return undefined; 76} 77 78function validateInput(input: Uint8Array, index: number): void { 79 if (index > input.length) { 80 throw new TypeError('Corrupt JPG, exceeded buffer limits'); 81 } 82} 83 84export const JPG: ImageData = { 85 validate: (input) => toHexString(input, 0, 2) === 'ffd8', 86 87 calculate(_input) { 88 let input = _input.slice(4); 89 let orientation: number | undefined; 90 let next: number; 91 while (input.length) { 92 const i = readUInt16BE(input, 0); 93 if (input[i] !== 0xff) { 94 input = input.slice(1); 95 continue; 96 } 97 if (isEXIF(input)) { 98 orientation = validateExifBlock(input, i); 99 } 100 validateInput(input, i); 101 next = input[i + 1]; 102 if (next === 0xc0 || next === 0xc1 || next === 0xc2) { 103 const size = extractSize(input, i + 5); 104 if (!orientation) { 105 return size; 106 } 107 return { 108 height: size.height, 109 orientation, 110 width: size.width, 111 }; 112 } 113 input = input.slice(i + 2); 114 } 115 throw new TypeError('Invalid JPG, no size found'); 116 }, 117};