From 8a151dddcd81800e59a0f4867cf276b7552553bf Mon Sep 17 00:00:00 2001 From: Ulrich Eckhardt Date: Tue, 16 Jul 2024 18:45:19 +0200 Subject: [PATCH 1/5] Mocha: Add two TIFF example files - tests/c2pa.tiff: small image - tests/c2pa_signed_ed25519.tiff: same image, but signed --- tests/c2pa.tiff | Bin 0 -> 1902 bytes tests/c2pa_signed_ed25519.tiff | Bin 0 -> 15858 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/c2pa.tiff create mode 100644 tests/c2pa_signed_ed25519.tiff diff --git a/tests/c2pa.tiff b/tests/c2pa.tiff new file mode 100644 index 0000000000000000000000000000000000000000..bcd82ecf5f89b9337addc5676e1d0c0dba98b364 GIT binary patch literal 1902 zcmebD)MChBU|?wAQV6h65#wcVaY_u>kzyvx-{F!JXtAI|NSL?H@u0y18!io&wnYq% zIszO#0u3&X3?Y3SeEiHzj2jLJFetXSGb#q$aA1*O<7gH9$PlHZ(iM~$vMbeWuK$XV zgbM-;0t^g{%nXb`n`D8Q5y}<;vYDW44j`Ku%4P+!*`RD@Ae#%y2I&`MWC5E4QZIxg zE{en!gR(*9i$m4N1KBc2YBYgtCZHJ5H4Ju)tYCH=I|G9~kj(}Z)dR8}fNB_kNDpK& z5Ztt@WzkmP1 z0AlVaIW$7R%`G7zEv>MyxVW^mtgNDC!P;>2mw zX3m^DcmDjXTlelgcI?E7lP7Q8x_$fJJy3f1{Q3L$U%wz}U}*Sz)RmAB7>)mtof2AK v2Lc12f#LAi6`+7%I(x8 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 Date: Tue, 16 Jul 2024 19:11:20 +0200 Subject: [PATCH 2/5] Mocha: Implement functional tests for TIFF images --- tests/tiff-processing.test.ts | 79 +++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 tests/tiff-processing.test.ts diff --git a/tests/tiff-processing.test.ts b/tests/tiff-processing.test.ts new file mode 100644 index 00000000..f486bead --- /dev/null +++ b/tests/tiff-processing.test.ts @@ -0,0 +1,79 @@ +import assert from 'node:assert/strict'; +import * as fs from 'node:fs/promises'; +import { Asset, JUMBF, Manifest } from '../src'; + +// location of the TIFF images +const baseDir = 'tests'; + +// test data sets with file names and expected outcomes +const testFiles = { + 'c2pa.tiff': { + jumbf: false, + valid: undefined, + }, + 'c2pa_signed_ed25519.tiff': { + jumbf: true, + valid: true, + }, +}; + +describe('Functional TIFF Reading Tests', function () { + this.timeout(0); + + for (const [filename, data] of Object.entries(testFiles)) { + describe(`test file ${filename}`, () => { + let buf: Buffer | undefined = undefined; + it(`loading test file`, async () => { + // load the file into a buffer + buf = await fs.readFile(`${baseDir}/${filename}`); + assert.ok(buf); + }); + + let asset: Asset.Asset | undefined = undefined; + it(`constructing the asset`, async function () { + if (!buf) { + this.skip(); + } + + // ensure it's a TIFF + assert.ok(Asset.TIFF.canRead(buf)); + + // construct the asset + asset = new Asset.TIFF(buf); + }); + + let jumbf: Uint8Array | undefined = undefined; + it(`extract the manifest JUMBF`, async function () { + if (!asset) { + this.skip(); + } + + // extract the C2PA manifest store in binary JUMBF format + jumbf = asset.getManifestJUMBF(); + if (data.jumbf) { + assert.ok(jumbf, 'no JUMBF found'); + } else { + assert.ok(jumbf === undefined, 'unexpected JUMBF found'); + } + }); + + if (data.jumbf) { + it(`validate manifest`, async function () { + if (!jumbf || !asset) { + this.skip(); + } + + // deserialize the JUMBF box structure + const superBox = JUMBF.SuperBox.fromBuffer(jumbf); + + // Read the manifest store from the JUMBF container + const manifests = Manifest.ManifestStore.read(superBox); + + // Validate the asset with the manifest + const validationResult = await manifests.validate(asset); + assert.equal(validationResult.isValid, data.valid); + }); + } + }); + } +}); From 59511352f4c0cd72b8b9ad56fee56eef9e597f23 Mon Sep 17 00:00:00 2001 From: Ulrich Eckhardt Date: Tue, 16 Jul 2024 18:56:05 +0200 Subject: [PATCH 3/5] TIFF Support: Export new asset type --- src/asset/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/asset/index.ts b/src/asset/index.ts index c0c93601..1643f1cc 100644 --- a/src/asset/index.ts +++ b/src/asset/index.ts @@ -1,4 +1,5 @@ export * from './BMFF'; export * from './JPEG'; export * from './PNG'; +export * from './TIFF'; export * from './types'; From 1aa67905b65eb8c49e613d9b00bf68efa8566e37 Mon Sep 17 00:00:00 2001 From: Ulrich Eckhardt Date: Tue, 16 Jul 2024 18:56:23 +0200 Subject: [PATCH 4/5] TIFF Support: Basic asset implementation This is able to process the two example files, which includes locating the JUMBF box with manifest data. --- src/asset/TIFF.ts | 173 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 src/asset/TIFF.ts diff --git a/src/asset/TIFF.ts b/src/asset/TIFF.ts new file mode 100644 index 00000000..288fbd31 --- /dev/null +++ b/src/asset/TIFF.ts @@ -0,0 +1,173 @@ +import { BaseAsset } from './BaseAsset'; +import { Asset } from './types'; + +// helper class to move around the file and read values +class Parser { + private pos: number; + + constructor( + private readonly data: Uint8Array, + private readonly little_endian: boolean, + ) { + this.pos = 0; + } + + public readUInt8(): number { + if (this.pos + 1 > this.data.length) throw new Error('Buffer underrun'); + return this.data[this.pos++]; + } + + public readUInt16(): number { + if (this.pos + 2 > this.data.length) throw new Error('Buffer underrun'); + if (this.little_endian) { + return this.data[this.pos++] + (this.data[this.pos++] << 8); + } else { + return (this.data[this.pos++] << 8) + this.data[this.pos++]; + } + } + + public readUInt32(): number { + if (this.pos + 4 > this.data.length) throw new Error('Buffer underrun'); + if (this.little_endian) { + return ( + this.data[this.pos++] + + (this.data[this.pos++] << 8) + + (this.data[this.pos++] << 16) + + (this.data[this.pos++] << 24) + ); + } else { + return ( + (this.data[this.pos++] << 24) + + (this.data[this.pos++] << 16) + + (this.data[this.pos++] << 8) + + this.data[this.pos++] + ); + } + } + + public seekTo(offset: number) { + if (offset > this.data.length) throw new Error('invalid offset'); + this.pos = offset; + } + + public skip(length: number) { + if (this.pos + length > this.data.length) throw new Error('Buffer underrun'); + this.pos += length; + } +} + +export class TIFF extends BaseAsset implements Asset { + private jumbf: Uint8Array | undefined; + + constructor(data: Uint8Array) { + super(data); + if (!TIFF.canRead(data)) throw new Error('Not a TIFF file'); + this.readChunks(); + } + + public static canRead(buf: Uint8Array): boolean { + if (buf.length < 4) return false; + + // first two bytes contain either "II" or "MM" and serve as + // BOM (byte order mark) for endianness of the TIFF file + const bom = buf[0] + (buf[1] << 8); + + // third and fourth bytes contain the value 42, in little or big + // endian representation, depending on the BOM. + let signature: number; + switch (bom) { + case 0x4949: // little endian + signature = buf[2] + (buf[3] << 8); + break; + case 0x4d4d: // big endian + signature = (buf[2] << 8) + buf[3]; + break; + default: + return false; + } + if (signature !== 0x002a) return false; + + return true; + } + + public dumpInfo() { + return ['TIFF file'].join('\n'); + } + + private readChunks() { + // The first two bytes contain either "II" or "MM" and serve as + // BOM (byte order mark) for the endianness of the TIFF file. + const bom = this.data[0] + (this.data[1] << 8); + if (bom !== 0x4949 && bom !== 0x4d4d) throw new Error('Invalid TIFF file'); + + const parser = new Parser(this.data, bom == 0x4949); + + // skip BOM + parser.skip(2); + + // verify magic number (42) + const magic = parser.readUInt16(); + if (magic != 0x002a) throw new Error('Invalid TIFF file'); + + // locate first IFD ("Image File Directory") + const ifdPosition = parser.readUInt32(); + parser.seekTo(ifdPosition); + + const ifdCount = parser.readUInt16(); + if (ifdCount < 1) throw new Error('Invalid TIFF file'); + for (let i = 0; i < ifdCount; i++) { + const tag = parser.readUInt16(); + const type = parser.readUInt16(); + const count = parser.readUInt32(); + const value_offset = parser.readUInt32(); + + let size: number; + switch (type) { + case 1: // BYTE + case 2: // ASCII + case 6: // SIGNED BYTE + case 17: // SIGNED SHORT + size = 1; + break; + case 3: // SHORT + case 16: // UNSIGNED SHORT + size = 2; + break; + case 4: // LONG + case 5: // UNSIGNED LONG + case 11: // FLOAT + case 12: // DOUBLE + size = 4; + break; + case 7: // UNDEFINED + case 10: // DOUBLE + size = 8; + break; + default: + throw new Error(`Unknown TIFF type ${type}`); + } + + // The C2PA Manifest Store is embedded into the TIFF as a tag + // with ID 52545 (0xcd41) and type UNDEFINED (7). + const manifestStoreTag = 0xcd41; + const manifestStoreType = 7; + if (type === manifestStoreType && tag === manifestStoreTag) { + const jumbf = this.data.slice(value_offset, value_offset + count * size); + + // Extract and validate the length stored in the JUMBF + // (JPEG Universal Media Fragment) itself. Note that it + // always uses big endian notation, regardless of the + // TIFF's endianess. + const jumbfParser = new Parser(jumbf, false); + if (jumbfParser.readUInt32() != count) + throw new Error('Mismatch between TIFF IDF length and JUMBF length'); + + this.jumbf = jumbf; + } + } + } + + public getManifestJUMBF(): Uint8Array | undefined { + return this.jumbf; + } +} From a1929bc7be547f8ddedf81d90d080b562fede0d3 Mon Sep 17 00:00:00 2001 From: Ulrich Eckhardt Date: Wed, 17 Jul 2024 10:47:44 +0200 Subject: [PATCH 5/5] README: Mark TIFF as supported --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1a28fee7..4bf373c8 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Anything that's not listed below is not currently planned to be implemented. - :white_check_mark: PNG - :white_check_mark: HEIC/HEIF - :x: GIF -- :x: TIFF +- :construction: TIFF (Basic support exists, but it is mostly unproven) - :x: WebP ### Supported assertions