Skip to content

Commit

Permalink
Merge pull request #88 from TrustNXT/feature/thumbnail-assertion
Browse files Browse the repository at this point in the history
Implement thumbnail assertion
  • Loading branch information
cyraxx authored Sep 2, 2024
2 parents 91c81d7 + 25908af commit 6066de5
Show file tree
Hide file tree
Showing 9 changed files with 157 additions and 14 deletions.
5 changes: 5 additions & 0 deletions .changeset/two-walls-thank.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@trustnxt/c2pa-ts': minor
---

Implement thumbnail assertion
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Anything that's not listed below is not currently planned to be implemented.
- :white_check_mark: Data Hash
- :white_check_mark: BMFF-Based Hash (except Merkle tree hashing)
- :x: General Boxes Hash
- :x: Thumbnail
- :white_check_mark: Thumbnail
- :white_check_mark: Actions (except action templates and metadata)
- :white_check_mark: Ingredient
- :white_check_mark: Metadata
Expand Down
6 changes: 6 additions & 0 deletions src/manifest/AssertionStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
UnknownAssertion,
} from './assertions';
import { AssertionLabels } from './assertions/AssertionLabels';
import { ThumbnailAssertion } from './assertions/ThumbnailAssertion';
import { Claim } from './Claim';
import * as raw from './rawTypes';
import { ManifestComponent, ValidationStatusCode } from './types';
Expand Down Expand Up @@ -66,6 +67,11 @@ export class AssertionStore implements ManifestComponent {
assertion = new IngredientAssertion();
} else if (AssertionLabels.metadataAssertions.includes(label.label)) {
assertion = new MetadataAssertion();
} else if (
box.descriptionBox.label.startsWith(AssertionLabels.thumbnailPrefix) ||
box.descriptionBox.label.startsWith(AssertionLabels.ingredientThumbnailPrefix)
) {
assertion = new ThumbnailAssertion();
} else {
assertion = new UnknownAssertion();
}
Expand Down
5 changes: 4 additions & 1 deletion src/manifest/Manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,6 @@ export class Manifest implements ManifestComponent {
if (assertion.label === AssertionLabels.actions || assertion.label === AssertionLabels.actionsV2) {
result.merge(await this.validateActionAssertion(assertionReference, assertion as ActionAssertion));
}
// TODO Validate references of thumbnail assertions

// Validate the hash reference to the assertion
if (await this.validateHashedReference(assertionReference)) {
Expand Down Expand Up @@ -387,6 +386,10 @@ export class Manifest implements ManifestComponent {
//if (!await this.validateHashedReference(referencedIngredient.manifestReference)) return false;
}

if (referencedIngredient.thumbnailReference) {
if (!(await this.validateHashedReference(referencedIngredient.thumbnailReference))) return false;
}

return true;
};

Expand Down
3 changes: 3 additions & 0 deletions src/manifest/assertions/IngredientAssertion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export class IngredientAssertion extends Assertion {
public instanceID?: string;
public relationship?: RelationshipType;
public manifestReference?: HashedURI;
public thumbnailReference?: HashedURI;

public readContentFromJUMBF(box: JUMBF.IBox, claim: Claim): void {
if (!(box instanceof JUMBF.CBORBox) || !this.uuid || !BinaryHelper.bufEqual(this.uuid, raw.UUIDs.cborAssertion))
Expand All @@ -57,6 +58,7 @@ export class IngredientAssertion extends Assertion {

this.relationship = content.relationship;
if (content.c2pa_manifest) this.manifestReference = claim.mapHashedURI(content.c2pa_manifest);
if (content.thumbnail) this.thumbnailReference = claim.mapHashedURI(content.thumbnail);
}

public generateJUMBFBoxForContent(claim: Claim): JUMBF.IBox {
Expand All @@ -72,6 +74,7 @@ export class IngredientAssertion extends Assertion {
relationship: this.relationship,
};
if (this.manifestReference) content.c2pa_manifest = claim.reverseMapHashedURI(this.manifestReference);
if (this.thumbnailReference) content.thumbnail = claim.reverseMapHashedURI(this.thumbnailReference);

const box = new JUMBF.CBORBox();
box.content = content;
Expand Down
115 changes: 115 additions & 0 deletions src/manifest/assertions/ThumbnailAssertion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import * as JUMBF from '../../jumbf';
import { BinaryHelper } from '../../util';
import { Claim } from '../Claim';
import * as raw from '../rawTypes';
import { ThumbnailType, ValidationStatusCode } from '../types';
import { ValidationError } from '../ValidationError';
import { Assertion } from './Assertion';
import { AssertionLabels } from './AssertionLabels';

export class ThumbnailAssertion extends Assertion {
public uuid = raw.UUIDs.embeddedFile;
public thumbnailType = ThumbnailType.Claim;
public mimeType?: string;
public content?: Uint8Array;

public readFromJUMBF(box: JUMBF.SuperBox, claim: Claim): void {
if (!box.descriptionBox?.label)
throw new ValidationError(ValidationStatusCode.AssertionRequiredMissing, box, 'Assertion is missing label');

if (box.descriptionBox.label.startsWith(AssertionLabels.thumbnailPrefix)) {
this.thumbnailType = ThumbnailType.Claim;
} else if (box.descriptionBox.label.startsWith(AssertionLabels.ingredientThumbnailPrefix)) {
this.thumbnailType = ThumbnailType.Ingredient;
} else {
throw new ValidationError(
ValidationStatusCode.AssertionRequiredMissing,
box,
'Thumbnail assertion has invalid label',
);
}

this.sourceBox = box;
this.uuid = box.descriptionBox.uuid;
this.label = box.descriptionBox.label;

if (!this.uuid || !BinaryHelper.bufEqual(this.uuid, raw.UUIDs.embeddedFile))
throw new ValidationError(
ValidationStatusCode.AssertionRequiredMissing,
this.sourceBox,
'Thumbnail assertion has invalid type',
);

const descriptionBox = box.contentBoxes.find(
(box): box is JUMBF.EmbeddedFileDescriptionBox => box instanceof JUMBF.EmbeddedFileDescriptionBox,
);
const contentBox = box.contentBoxes.find(
(box): box is JUMBF.EmbeddedFileBox => box instanceof JUMBF.EmbeddedFileBox,
);
if (!descriptionBox?.mediaType || !contentBox?.content?.length)
throw new ValidationError(
ValidationStatusCode.AssertionRequiredMissing,
box,
'Thumbnail assertion is missing file content or description',
);

this.content = contentBox.content;
this.mimeType = descriptionBox.mediaType;
}

public generateJUMBFBox(): JUMBF.SuperBox {
if (!this.content || !this.mimeType) throw new Error('Thumbnail assertion is missing content or type');

const box = new JUMBF.SuperBox();

box.descriptionBox = new JUMBF.DescriptionBox();
box.descriptionBox.label = this.fullLabel;
if (this.uuid) box.descriptionBox.uuid = this.uuid;

const descriptionBox = new JUMBF.EmbeddedFileDescriptionBox();
descriptionBox.mediaType = this.mimeType;
box.contentBoxes.push(descriptionBox);

const contentBox = new JUMBF.EmbeddedFileBox();
contentBox.content = this.content;
box.contentBoxes.push(contentBox);

this.sourceBox = box;
return box;
}

/**
* Creates a new thumbnail assertion
* @param imageType Image format type (without `image/`)
* @param content Binary thumbnail content
* @param thumbnailType Thumbnail type (claim or ingredient)
* @param suffix Optional suffix for ingredient thumbnails
*/
public static create(
imageType: string,
content: Uint8Array,
thumbnailType: ThumbnailType,
suffix?: string,
): ThumbnailAssertion {
const assertion = new ThumbnailAssertion();
assertion.mimeType = `image/${imageType}`;
assertion.content = content;
assertion.thumbnailType = thumbnailType;
if (thumbnailType === ThumbnailType.Claim) {
if (suffix) throw new Error('Suffix is not allowed for claim thumbnails');
assertion.label = AssertionLabels.thumbnailPrefix + imageType;
} else {
assertion.label =
AssertionLabels.ingredientThumbnailPrefix + (suffix ? '_' + suffix : '') + '.' + imageType;
}
return assertion;
}

// These are not used because we override readFromJUMBF() and generateJUMBFBox()
public readContentFromJUMBF(): void {
throw new Error('Method not implemented.');
}
public generateJUMBFBoxForContent(): JUMBF.IBox {
throw new Error('Method not implemented.');
}
}
1 change: 1 addition & 0 deletions src/manifest/assertions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export * from './DataHashAssertion';
export * from './IngredientAssertion';
export * from './MetadataAssertion';
export * from './SchemaOrgAssertion';
export * from './ThumbnailAssertion';
export * from './UnknownAssertion';
5 changes: 5 additions & 0 deletions src/manifest/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,3 +245,8 @@ export interface MetadataEntry {
name: string;
value: MetadataValue;
}

export enum ThumbnailType {
Claim,
Ingredient,
}
29 changes: 17 additions & 12 deletions tests/manifest/assertions/IngredientAssertion.test.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
import assert from 'node:assert/strict';
import * as bin from 'typed-binary';
import { CBORBox, SuperBox } from '../../../src/jumbf';
import { Assertion, Claim, IngredientAssertion } from '../../../src/manifest';
import { Assertion, Claim, HashedURI, IngredientAssertion } from '../../../src/manifest';
import * as raw from '../../../src/manifest/rawTypes';
import { BinaryHelper } from '../../../src/util';

describe('IngredientAssertion Tests', function () {
this.timeout(0);

const claim = new Claim();
claim.defaultAlgorithm = 'SHA-256';

// taken from adobe-20220124-CA.jpg.jumbf.text
const serializedString =
'000001576a756d62000000296a756d6463626f7200110010800000aa00389b7103633270612e696e6772656469656e74000000012663626f72a66864633a7469746c6565412e6a70676964633a666f726d61746a696d6167652f6a7065676a646f63756d656e744944782c786d702e6469643a38313365653432322d393733362d346364632d396265362d3465333565643865343163626a696e7374616e63654944782c786d702e6969643a38313365653432322d393733362d346364632d396265362d3465333565643865343163626c72656c6174696f6e7368697068706172656e744f66697468756d626e61696ca26375726c783973656c66236a756d62663d633270612e617373657274696f6e732f633270612e7468756d626e61696c2e696e6772656469656e742e6a70656764686173685820cf9e5b46a152bff1dd42516953de8050fc039c73168bf5d555a9de74a13b9317';

const thumbnailHash = new Uint8Array([
207, 158, 91, 70, 161, 82, 191, 241, 221, 66, 81, 105, 83, 222, 128, 80, 252, 3, 156, 115, 22, 139, 245, 213,
85, 169, 222, 116, 161, 59, 147, 23,
]);

let superBox: SuperBox;
it('read a JUMBF box', function () {
const buffer = BinaryHelper.fromHexString(serializedString);
Expand All @@ -40,10 +46,7 @@ describe('IngredientAssertion Tests', function () {
relationship: 'parentOf',
thumbnail: {
url: 'self#jumbf=c2pa.assertions/c2pa.thumbnail.ingredient.jpeg',
hash: new Uint8Array([
207, 158, 91, 70, 161, 82, 191, 241, 221, 66, 81, 105, 83, 222, 128, 80, 252, 3, 156, 115, 22, 139,
245, 213, 85, 169, 222, 116, 161, 59, 147, 23,
]),
hash: thumbnailHash,
},
});
superBox = box;
Expand All @@ -66,6 +69,11 @@ describe('IngredientAssertion Tests', function () {
assert.equal(ingredientAssertion.instanceID, 'xmp.iid:813ee422-9736-4cdc-9be6-4e35ed8e41cb');
assert.equal(ingredientAssertion.relationship, 'parentOf');
assert.equal(ingredientAssertion.manifestReference, undefined);
assert.deepEqual(ingredientAssertion.thumbnailReference, {
uri: 'self#jumbf=c2pa.assertions/c2pa.thumbnail.ingredient.jpeg',
hash: thumbnailHash,
algorithm: 'SHA-256',
} as HashedURI);

assertion = ingredientAssertion;
});
Expand All @@ -91,13 +99,10 @@ describe('IngredientAssertion Tests', function () {
documentID: 'xmp.did:813ee422-9736-4cdc-9be6-4e35ed8e41cb',
instanceID: 'xmp.iid:813ee422-9736-4cdc-9be6-4e35ed8e41cb',
relationship: 'parentOf',
// TODO: thumbnail: {
// url: 'self#jumbf=c2pa.assertions/c2pa.thumbnail.ingredient.jpeg',
// hash: new Uint8Array([
// 207, 158, 91, 70, 161, 82, 191, 241, 221, 66, 81, 105, 83, 222, 128, 80, 252, 3, 156, 115, 22, 139,
// 245, 213, 85, 169, 222, 116, 161, 59, 147, 23,
// ]),
// },
thumbnail: {
url: 'self#jumbf=c2pa.assertions/c2pa.thumbnail.ingredient.jpeg',
hash: thumbnailHash,
},
});
});
});

0 comments on commit 6066de5

Please sign in to comment.