|
| 1 | +import { BaseAsset } from './BaseAsset'; |
| 2 | +import { Asset } from './types'; |
| 3 | + |
| 4 | +// helper class to move around the file and read values |
| 5 | +class Parser { |
| 6 | + private pos: number; |
| 7 | + |
| 8 | + constructor( |
| 9 | + private readonly data: Uint8Array, |
| 10 | + private readonly little_endian: boolean, |
| 11 | + ) { |
| 12 | + this.pos = 0; |
| 13 | + } |
| 14 | + |
| 15 | + public readUInt8(): number { |
| 16 | + if (this.pos + 1 > this.data.length) throw new Error('Buffer underrun'); |
| 17 | + return this.data[this.pos++]; |
| 18 | + } |
| 19 | + |
| 20 | + public readUInt16(): number { |
| 21 | + if (this.pos + 2 > this.data.length) throw new Error('Buffer underrun'); |
| 22 | + if (this.little_endian) { |
| 23 | + return this.data[this.pos++] + (this.data[this.pos++] << 8); |
| 24 | + } else { |
| 25 | + return (this.data[this.pos++] << 8) + this.data[this.pos++]; |
| 26 | + } |
| 27 | + } |
| 28 | + |
| 29 | + public readUInt32(): number { |
| 30 | + if (this.pos + 4 > this.data.length) throw new Error('Buffer underrun'); |
| 31 | + if (this.little_endian) { |
| 32 | + return ( |
| 33 | + this.data[this.pos++] + |
| 34 | + (this.data[this.pos++] << 8) + |
| 35 | + (this.data[this.pos++] << 16) + |
| 36 | + (this.data[this.pos++] << 24) |
| 37 | + ); |
| 38 | + } else { |
| 39 | + return ( |
| 40 | + (this.data[this.pos++] << 24) + |
| 41 | + (this.data[this.pos++] << 16) + |
| 42 | + (this.data[this.pos++] << 8) + |
| 43 | + this.data[this.pos++] |
| 44 | + ); |
| 45 | + } |
| 46 | + } |
| 47 | + |
| 48 | + public seekTo(offset: number) { |
| 49 | + if (offset > this.data.length) throw new Error('invalid offset'); |
| 50 | + this.pos = offset; |
| 51 | + } |
| 52 | + |
| 53 | + public skip(length: number) { |
| 54 | + if (this.pos + length > this.data.length) throw new Error('Buffer underrun'); |
| 55 | + this.pos += length; |
| 56 | + } |
| 57 | +} |
| 58 | + |
| 59 | +export class TIFF extends BaseAsset implements Asset { |
| 60 | + private jumbf: Uint8Array | undefined; |
| 61 | + |
| 62 | + constructor(data: Uint8Array) { |
| 63 | + super(data); |
| 64 | + if (!TIFF.canRead(data)) throw new Error('Not a TIFF file'); |
| 65 | + this.readChunks(); |
| 66 | + } |
| 67 | + |
| 68 | + public static canRead(buf: Uint8Array): boolean { |
| 69 | + if (buf.length < 4) return false; |
| 70 | + |
| 71 | + // first two bytes contain either "II" or "MM" and serve as |
| 72 | + // BOM (byte order mark) for endianness of the TIFF file |
| 73 | + const bom = buf[0] + (buf[1] << 8); |
| 74 | + |
| 75 | + // third and fourth bytes contain the value 42, in little or big |
| 76 | + // endian representation, depending on the BOM. |
| 77 | + let signature: number; |
| 78 | + switch (bom) { |
| 79 | + case 0x4949: // little endian |
| 80 | + signature = buf[2] + (buf[3] << 8); |
| 81 | + break; |
| 82 | + case 0x4d4d: // big endian |
| 83 | + signature = (buf[2] << 8) + buf[3]; |
| 84 | + break; |
| 85 | + default: |
| 86 | + return false; |
| 87 | + } |
| 88 | + if (signature !== 0x002a) return false; |
| 89 | + |
| 90 | + return true; |
| 91 | + } |
| 92 | + |
| 93 | + public dumpInfo() { |
| 94 | + return ['TIFF file'].join('\n'); |
| 95 | + } |
| 96 | + |
| 97 | + private readChunks() { |
| 98 | + // The first two bytes contain either "II" or "MM" and serve as |
| 99 | + // BOM (byte order mark) for the endianness of the TIFF file. |
| 100 | + const bom = this.data[0] + (this.data[1] << 8); |
| 101 | + if (bom !== 0x4949 && bom !== 0x4d4d) throw new Error('Invalid TIFF file'); |
| 102 | + |
| 103 | + const parser = new Parser(this.data, bom == 0x4949); |
| 104 | + |
| 105 | + // skip BOM |
| 106 | + parser.skip(2); |
| 107 | + |
| 108 | + // verify magic number (42) |
| 109 | + const magic = parser.readUInt16(); |
| 110 | + if (magic != 0x002a) throw new Error('Invalid TIFF file'); |
| 111 | + |
| 112 | + // locate first IFD ("Image File Directory") |
| 113 | + const ifdPosition = parser.readUInt32(); |
| 114 | + parser.seekTo(ifdPosition); |
| 115 | + |
| 116 | + const ifdCount = parser.readUInt16(); |
| 117 | + if (ifdCount < 1) throw new Error('Invalid TIFF file'); |
| 118 | + for (let i = 0; i < ifdCount; i++) { |
| 119 | + const tag = parser.readUInt16(); |
| 120 | + const type = parser.readUInt16(); |
| 121 | + const count = parser.readUInt32(); |
| 122 | + const value_offset = parser.readUInt32(); |
| 123 | + |
| 124 | + let size: number; |
| 125 | + switch (type) { |
| 126 | + case 1: // BYTE |
| 127 | + case 2: // ASCII |
| 128 | + case 6: // SIGNED BYTE |
| 129 | + case 17: // SIGNED SHORT |
| 130 | + size = 1; |
| 131 | + break; |
| 132 | + case 3: // SHORT |
| 133 | + case 16: // UNSIGNED SHORT |
| 134 | + size = 2; |
| 135 | + break; |
| 136 | + case 4: // LONG |
| 137 | + case 5: // UNSIGNED LONG |
| 138 | + case 11: // FLOAT |
| 139 | + case 12: // DOUBLE |
| 140 | + size = 4; |
| 141 | + break; |
| 142 | + case 7: // UNDEFINED |
| 143 | + case 10: // DOUBLE |
| 144 | + size = 8; |
| 145 | + break; |
| 146 | + default: |
| 147 | + throw new Error(`Unknown TIFF type ${type}`); |
| 148 | + } |
| 149 | + |
| 150 | + // The C2PA Manifest Store is embedded into the TIFF as a tag |
| 151 | + // with ID 52545 (0xcd41) and type UNDEFINED (7). |
| 152 | + const manifestStoreTag = 0xcd41; |
| 153 | + const manifestStoreType = 7; |
| 154 | + if (type === manifestStoreType && tag === manifestStoreTag) { |
| 155 | + const jumbf = this.data.slice(value_offset, value_offset + count * size); |
| 156 | + |
| 157 | + // Extract and validate the length stored in the JUMBF |
| 158 | + // (JPEG Universal Media Fragment) itself. Note that it |
| 159 | + // always uses big endian notation, regardless of the |
| 160 | + // TIFF's endianess. |
| 161 | + const jumbfParser = new Parser(jumbf, false); |
| 162 | + if (jumbfParser.readUInt32() != count) |
| 163 | + throw new Error('Mismatch between TIFF IDF length and JUMBF length'); |
| 164 | + |
| 165 | + this.jumbf = jumbf; |
| 166 | + } |
| 167 | + } |
| 168 | + } |
| 169 | + |
| 170 | + public getManifestJUMBF(): Uint8Array | undefined { |
| 171 | + return this.jumbf; |
| 172 | + } |
| 173 | +} |
0 commit comments