• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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};