Skip to content

Commit fda6bee

Browse files
committed
fix(presentation-2): handle malformed manifests with incorrect structure
- Fix annotation parsing when @type is not oa:Annotation (e.g., dctypes:Image) by detecting annotations via presence of 'on' property in images array - Handle v2-context manifests that use v3 structure (items instead of sequences) by converting v3 Canvas/Annotation/body structure to v2 before traversal Fixes issues with St. Andrews manifests that use non-standard structures
1 parent 00724d3 commit fda6bee

3 files changed

Lines changed: 170 additions & 0 deletions

File tree

__tests__/presentation-2-parser/upgrade.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import goettingen from '../../fixtures/presentation-2/uni-goettingen.json';
3434
import villanovaManifest from '../../fixtures/presentation-2/villanova-manifest.json';
3535
import wikimediaProxy from '../../fixtures/presentation-2/wikimedia-proxy.json';
3636
import malformedImageAnnotation from '../../fixtures/presentation-2/malformed-image-annotation.json';
37+
import stAndrewsMalformed from '../../fixtures/presentation-2/st-andrews-malformed.json';
3738
import { convertPresentation2, presentation2to3 } from '../../src/presentation-2';
3839

3940
describe('Presentation 2 to 3', () => {
@@ -2636,4 +2637,37 @@ describe('Presentation 2 to 3', () => {
26362637
expect(annotation?.motivation).toEqual('painting');
26372638
expect(annotation?.body).toBeDefined();
26382639
});
2640+
2641+
test('V2 context manifest with V3 structure (items instead of sequences)', () => {
2642+
// Some manifests (e.g., St. Andrews) have @context: "http://iiif.io/api/presentation/2/context.json"
2643+
// but use v3-style structure with items array instead of v2 sequences[0].canvases.
2644+
// The converter should detect this and handle it appropriately.
2645+
// Note: We don't validate strictly here because the source manifest may be missing
2646+
// required properties like canvas width/height - we just verify the structure converts.
2647+
const result = presentation2to3.traverseManifest(stAndrewsMalformed as any);
2648+
2649+
// Verify the canvas was correctly converted
2650+
expect(result.items).toBeDefined();
2651+
expect(result.items?.length).toBeGreaterThan(0);
2652+
2653+
const canvas = result.items?.[0];
2654+
expect(canvas).toBeDefined();
2655+
expect(canvas?.id).toEqual('https://collections.st-andrews.ac.uk/762345/manifest/canvas/406403');
2656+
expect(canvas?.type).toEqual('Canvas');
2657+
2658+
// Verify the annotation page and annotation were correctly converted
2659+
const annotationPage = canvas?.items?.[0];
2660+
expect(annotationPage).toBeDefined();
2661+
expect(annotationPage?.type).toEqual('AnnotationPage');
2662+
2663+
const annotation = annotationPage?.items?.[0];
2664+
expect(annotation).toBeDefined();
2665+
expect(annotation?.type).toEqual('Annotation');
2666+
expect(annotation?.motivation).toEqual('painting');
2667+
2668+
// Verify the body (3D model) was correctly converted
2669+
expect(annotation?.body).toBeDefined();
2670+
expect(annotation?.body?.id).toEqual('https://collections.st-andrews.ac.uk/media/406403/406403.glb');
2671+
expect(annotation?.body?.format).toEqual('model/gltf-binary');
2672+
});
26392673
});
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
{
2+
"@context": "http://iiif.io/api/presentation/2/context.json",
3+
"@type": "sc:Manifest",
4+
"@id": "https://collections.st-andrews.ac.uk/762345/manifest",
5+
"label": "Dura Den Fossil Fish",
6+
"description": "Slab containing two fossilised Holopychius flemingi fish, which lived during the Devonian period, around 400 million years ago. The unusual bone structure of the fish, with fins attached to stumps, attracted the attention of prominent scientists including Charles Lyell. This fueled the debate on evolution as they could be interpreted as examples of Darwin's 'Transitional Forms' between fish with fins and animals with legs.<br/> <br/>The fossil is unusual as the fish are perfectly formed. Normally as they are subjected to lots of heat and pressure, flattened and often scavenged before found, you do not get a full skeleton. The fish appear clustered together as they were buried during a sandstorm whilst they were forced together as the rivers were drying up.",
7+
"attribution": "© The University of St Andrews",
8+
"logo": "https://collections.st-andrews.ac.uk/uv/logo.png",
9+
"metadata": [
10+
{ "label": "Identifier", "value": "GE1049" },
11+
{ "label": "Title", "value": "Dura Den Fossil Fish" },
12+
{
13+
"label": "Record URL",
14+
"value": "<a href='https://collections.st-andrews.ac.uk/item/dura-den-fossil-fish/762345'>https://collections.st-andrews.ac.uk/item/dura-den-fossil-fish/762345</a>"
15+
},
16+
{
17+
"label": "Subjects",
18+
"value": " <a href='https://collections.st-andrews.ac.uk/search/?query=Subject:\"geology\"&form=grid&mode=query'>geology</a>,<a href='https://collections.st-andrews.ac.uk/search/?query=Subject:\"fossil\"&form=grid&mode=query'>fossil</a>,<a href='https://collections.st-andrews.ac.uk/search/?query=Subject:\"fish\"&form=grid&mode=query'>fish</a>,<a href='https://collections.st-andrews.ac.uk/search/?query=Subject:\"dura%20den\"&form=grid&mode=query'>dura den</a>"
19+
},
20+
{ "label": "Department", "value": "Museums" },
21+
{
22+
"label": "Collection",
23+
"value": "<a href='https://collections.st-andrews.ac.uk/collection/geology-collection/1004098'>Geology Collection</a>"
24+
},
25+
{ "label": "Record level", "value": "Item" },
26+
{
27+
"label": "Conditions of use",
28+
"value": "CC BY-NC Creative Commons Attribution-NonCommercial 4.0 International Public License"
29+
},
30+
{ "label": "Credit line", "value": "Image Courtesy of the University of St Andrews Library, ID GE1049" },
31+
{ "label": "IRN", "value": "762345" },
32+
{
33+
"label": "Image delivery",
34+
"value": "Working in partnership with <a href='https://kakadusoftware.com/'>Kakadu</a> to deliver high quality image reproductions via IIIF."
35+
}
36+
],
37+
"items": [
38+
{
39+
"id": "https://collections.st-andrews.ac.uk/762345/manifest/canvas/406403",
40+
"type": "Canvas",
41+
"items": [
42+
{
43+
"id": "https://collections.st-andrews.ac.uk/762345/manifest/canvas/406403/annotationpage/0",
44+
"type": "AnnotationPage",
45+
"items": [
46+
{
47+
"id": "https://collections.st-andrews.ac.uk/762345/manifest/canvas/406403/annotation/0",
48+
"type": "Annotation",
49+
"motivation": "painting",
50+
"body": {
51+
"id": "https://collections.st-andrews.ac.uk/media/406403/406403.glb",
52+
"type": "Model",
53+
"format": "model/gltf-binary"
54+
},
55+
"target": "https://collections.st-andrews.ac.uk/762345/manifest/canvas/406403"
56+
}
57+
]
58+
}
59+
]
60+
}
61+
]
62+
}

src/presentation-2/traverse.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,18 @@ export class Traverse<
229229
}
230230

231231
traverseManifestItems(manifest: Manifest): Manifest {
232+
// Handle malformed manifests that have v2 @context but use v3 structure (items instead of sequences).
233+
// Convert v3 items structure to v2 sequences structure before traversal.
234+
if (!manifest.sequences && (manifest as any).items && Array.isArray((manifest as any).items)) {
235+
manifest.sequences = [
236+
{
237+
'@id': `${manifest['@id']}/sequence/0`,
238+
'@type': 'sc:Sequence',
239+
canvases: (manifest as any).items.map((item: any) => this.convertV3CanvasToV2(item)),
240+
} as any,
241+
];
242+
delete (manifest as any).items;
243+
}
232244
if (manifest.sequences) {
233245
manifest.sequences = manifest.sequences.map((sequence) => this.traverseSequence(sequence));
234246
}
@@ -238,6 +250,68 @@ export class Traverse<
238250
return manifest;
239251
}
240252

253+
/**
254+
* Convert a v3-style Canvas to v2-style Canvas structure.
255+
* Handles manifests with v2 context but v3 structure.
256+
*/
257+
convertV3CanvasToV2(canvas: any): any {
258+
// If it's already v2 style, return as-is
259+
if (canvas['@type'] || canvas['@id']) {
260+
return canvas;
261+
}
262+
263+
const v2Canvas: any = {
264+
'@id': canvas.id,
265+
'@type': 'sc:Canvas',
266+
label: canvas.label,
267+
height: canvas.height,
268+
width: canvas.width,
269+
};
270+
271+
// Convert v3 items (AnnotationPages) to v2 images array
272+
if (canvas.items && Array.isArray(canvas.items)) {
273+
v2Canvas.images = [];
274+
for (const annotationPage of canvas.items) {
275+
if (annotationPage.items && Array.isArray(annotationPage.items)) {
276+
for (const annotation of annotationPage.items) {
277+
v2Canvas.images.push(this.convertV3AnnotationToV2(annotation, canvas.id));
278+
}
279+
}
280+
}
281+
}
282+
283+
return v2Canvas;
284+
}
285+
286+
/**
287+
* Convert a v3-style Annotation to v2-style Annotation structure.
288+
*/
289+
convertV3AnnotationToV2(annotation: any, canvasId: string): any {
290+
return {
291+
'@id': annotation.id,
292+
'@type': 'oa:Annotation',
293+
motivation: annotation.motivation === 'painting' ? 'sc:painting' : annotation.motivation,
294+
on: annotation.target || canvasId,
295+
resource: this.convertV3BodyToV2Resource(annotation.body),
296+
};
297+
}
298+
299+
/**
300+
* Convert a v3-style body to v2-style resource.
301+
*/
302+
convertV3BodyToV2Resource(body: any): any {
303+
if (!body) return undefined;
304+
305+
return {
306+
'@id': body.id,
307+
'@type': body.type === 'Image' ? 'dctypes:Image' : `dctypes:${body.type}`,
308+
format: body.format,
309+
height: body.height,
310+
width: body.width,
311+
service: body.service,
312+
};
313+
}
314+
241315
traverseSequence(sequence: Sequence): T['Sequence'] {
242316
return this.traverseType(
243317
this.traverseDescriptive(this.traverseLinking(this.traverseSequenceItems(sequence))),

0 commit comments

Comments
 (0)