From 9712c5c9733e17aacf720c3c1fabb02481f7af8a Mon Sep 17 00:00:00 2001 From: Ulrich Eckhardt Date: Tue, 16 Jul 2024 11:53:15 +0200 Subject: [PATCH 1/5] GIF Support: Add a very basic implementation This doesn't support any manifest operations yet but it is able to load an example GIF successfully. --- src/asset/GIF.ts | 132 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 src/asset/GIF.ts diff --git a/src/asset/GIF.ts b/src/asset/GIF.ts new file mode 100644 index 00000000..ebbd4ff3 --- /dev/null +++ b/src/asset/GIF.ts @@ -0,0 +1,132 @@ +import { BaseAsset } from './BaseAsset'; +import { Asset } from './types'; + +class Parser { + private pos: number; + + constructor(private readonly data: Uint8Array) { + 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'); + return this.data[this.pos++] + (this.data[this.pos++] << 8); + } + + public skip(length: number) { + if (this.pos + length > this.data.length) throw new Error('Buffer underrun'); + this.pos += length; + } +} + +export class GIF extends BaseAsset implements Asset { + constructor(data: Uint8Array) { + super(data); + if (!GIF.canRead(data)) throw new Error('Not a GIF file'); + this.readChunks(); + } + + public static canRead(buf: Uint8Array): boolean { + return ( + buf.length > 6 && + buf[0] === 0x47 && // G + buf[1] === 0x49 && // I + buf[2] === 0x46 && // F + buf[3] === 0x38 && // 8 + (buf[4] === 0x39 || buf[4] === 0x37) && // 9 or 7 + buf[5] === 0x61 // a + ); + } + + public dumpInfo() { + return ['GIF file'].join('\n'); + } + + private readChunks() { + const parser = new Parser(this.data); + + // skip over the GIF87a or GIF89a signature header + parser.skip(6); + + // read the "Logical Screen Descriptor" + const logicalScreenWidth = parser.readUInt16(); + const logicalScreenHeight = parser.readUInt16(); + const packedFields = parser.readUInt8(); + const backgroundColorIndex = parser.readUInt8(); + const pixelAspectRatio = parser.readUInt8(); + + // unpack the packed fields + const globalColorTableFlag = packedFields & 0x80; + const colorResolution = (packedFields & 0x70) >> 4; + const sortFlag = packedFields & 0x08; + const globalColorTableSize = 1 << ((packedFields & 0x07) + 1); + + // skip over the "Global Color Table" if it is present + if (globalColorTableFlag) { + parser.skip(3 * globalColorTableSize); + + if (backgroundColorIndex >= globalColorTableSize) + throw new Error('Malformed GIF (invalid background color index)'); + } + + // iterate over blocks: + // Every block starts with an exclamation mark ("!") or comma (","). + // The end of the image is marked with a semicolon (";"). + for (;;) { + const blockType = parser.readUInt8(); + switch (blockType) { + case 0x21: // Extension block ("!") + { + const extensionBlockType = parser.readUInt8(); + if (extensionBlockType !== 0xf9) + throw new Error('Malformed GIF (invalid extension block type)'); + const extensionBlockDataLength = parser.readUInt8(); + parser.skip(extensionBlockDataLength); + if (parser.readUInt8() !== 0) throw new Error('Malformed GIF (invalid block terminator)'); + } + break; + case 0x2c: // Image Descriptor (",") + { + // skipping size and position + parser.skip(8); + + // decode packed fields + const packedImageDescriptorFields = parser.readUInt8(); + + const localColorTableFlag = packedImageDescriptorFields & 0x80; + const interlaceFlag = packedImageDescriptorFields & 0x40; + const sortFlag = packedImageDescriptorFields & 0x20; + const localColorTableSize = 1 << ((packedImageDescriptorFields & 0x07) + 1); + + // skip over the "Local Color Table" if it is present + if (localColorTableFlag) { + parser.skip(3 * localColorTableSize); + } + + const lzwCodeSize = parser.readUInt8(); + + // decode the image data blocks + for (;;) { + const blockSize = parser.readUInt8(); + if (blockSize === 0) break; // terminator + parser.skip(blockSize); + } + } + break; + case 0x3b: // Trailer (";") + return; + default: + throw new Error('Malformed GIF (invalid block type)'); + } + } + } + + public getManifestJUMBF(): Uint8Array | undefined { + return undefined; + } +} From 7307d6c54d7ecf2e75e104ff6d1c8b62ce55ef45 Mon Sep 17 00:00:00 2001 From: Ulrich Eckhardt Date: Tue, 16 Jul 2024 11:52:18 +0200 Subject: [PATCH 2/5] GIF 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..8e5c6794 100644 --- a/src/asset/index.ts +++ b/src/asset/index.ts @@ -1,4 +1,5 @@ export * from './BMFF'; +export * from './GIF'; export * from './JPEG'; export * from './PNG'; export * from './types'; From a4c470ef5314b0bc3e2f40c55919fe0280ee533e Mon Sep 17 00:00:00 2001 From: Ulrich Eckhardt Date: Tue, 16 Jul 2024 11:53:25 +0200 Subject: [PATCH 3/5] Mocha: Add GIF test file This is a very small GIF with the text "c2pa" and without any manifest. --- tests/c2pa.gif | Bin 0 -> 303 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/c2pa.gif diff --git a/tests/c2pa.gif b/tests/c2pa.gif new file mode 100644 index 0000000000000000000000000000000000000000..ec6d769db0aca3301cfe0971118875fcbb401e21 GIT binary patch literal 303 zcmZ?wbhEHblw}ZM_^QEh%gyb6Lc)Wzw8w>okBf^RmzF*$D|=E=@uafyNmbR;nwqC| zbqU3>i=LhreSI${PJA_O+UuD!U(cQUX8!ySTep7NyZ6hn zV_#03_;T{(k6X8X+`j$e-n~B`KK%Lg>F?*yf4_hK_v_dHzkmP#`}hC Date: Tue, 16 Jul 2024 11:53:01 +0200 Subject: [PATCH 4/5] Mocha: Add a functional test for GIF processing --- tests/gif-processing.test.ts | 73 ++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 tests/gif-processing.test.ts diff --git a/tests/gif-processing.test.ts b/tests/gif-processing.test.ts new file mode 100644 index 00000000..fd399ffb --- /dev/null +++ b/tests/gif-processing.test.ts @@ -0,0 +1,73 @@ +import assert from 'node:assert/strict'; +import * as fs from 'node:fs/promises'; +import { Asset, JUMBF, Manifest } from '../src'; + +// location of the GIF images +const baseDir = 'tests'; + +// test data sets with file names and expected outcomes +const testFiles = { + 'c2pa.gif': { + jumbf: false, + valid: undefined, + }, +}; + +describe('Functional GIF Reading Tests', function () { + 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 GIF + assert.ok(Asset.GIF.canRead(buf)); + + // construct the asset + asset = new Asset.GIF(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 ff43d8edd3f15a3bd25a463855b217aaadd9b050 Mon Sep 17 00:00:00 2001 From: Ulrich Eckhardt Date: Tue, 16 Jul 2024 18:16:28 +0200 Subject: [PATCH 5/5] README: Mark GIF as work in progress --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1a28fee7..0bc4fb41 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Anything that's not listed below is not currently planned to be implemented. - :white_check_mark: JPEG - :white_check_mark: PNG - :white_check_mark: HEIC/HEIF -- :x: GIF +- :construction: GIF - :x: TIFF - :x: WebP