Skip to content

Commit 99e9797

Browse files
TIFF Support: Basic asset implementation
This is able to process the two example files, which includes locating the JUMBF box with manifest data.
1 parent 8683243 commit 99e9797

File tree

1 file changed

+173
-0
lines changed

1 file changed

+173
-0
lines changed

src/asset/TIFF.ts

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
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

Comments
 (0)