-
Notifications
You must be signed in to change notification settings - Fork 3
fix(presentation-2): handle malformed manifests with incorrect structure #59
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| { | ||
| "@context": "http://iiif.io/api/presentation/2/context.json", | ||
| "@type": "sc:Manifest", | ||
| "@id": "https://collections.st-andrews.ac.uk/762345/manifest", | ||
| "label": "Dura Den Fossil Fish", | ||
| "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.", | ||
| "attribution": "© The University of St Andrews", | ||
| "logo": "https://collections.st-andrews.ac.uk/uv/logo.png", | ||
| "metadata": [ | ||
| { "label": "Identifier", "value": "GE1049" }, | ||
| { "label": "Title", "value": "Dura Den Fossil Fish" }, | ||
| { | ||
| "label": "Record URL", | ||
| "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>" | ||
| }, | ||
| { | ||
| "label": "Subjects", | ||
| "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>" | ||
| }, | ||
| { "label": "Department", "value": "Museums" }, | ||
| { | ||
| "label": "Collection", | ||
| "value": "<a href='https://collections.st-andrews.ac.uk/collection/geology-collection/1004098'>Geology Collection</a>" | ||
| }, | ||
| { "label": "Record level", "value": "Item" }, | ||
| { | ||
| "label": "Conditions of use", | ||
| "value": "CC BY-NC Creative Commons Attribution-NonCommercial 4.0 International Public License" | ||
| }, | ||
| { "label": "Credit line", "value": "Image Courtesy of the University of St Andrews Library, ID GE1049" }, | ||
| { "label": "IRN", "value": "762345" }, | ||
| { | ||
| "label": "Image delivery", | ||
| "value": "Working in partnership with <a href='https://kakadusoftware.com/'>Kakadu</a> to deliver high quality image reproductions via IIIF." | ||
| } | ||
| ], | ||
| "items": [ | ||
| { | ||
| "id": "https://collections.st-andrews.ac.uk/762345/manifest/canvas/406403", | ||
| "type": "Canvas", | ||
| "items": [ | ||
| { | ||
| "id": "https://collections.st-andrews.ac.uk/762345/manifest/canvas/406403/annotationpage/0", | ||
| "type": "AnnotationPage", | ||
| "items": [ | ||
| { | ||
| "id": "https://collections.st-andrews.ac.uk/762345/manifest/canvas/406403/annotation/0", | ||
| "type": "Annotation", | ||
| "motivation": "painting", | ||
| "body": { | ||
| "id": "https://collections.st-andrews.ac.uk/media/406403/406403.glb", | ||
| "type": "Model", | ||
| "format": "model/gltf-binary" | ||
| }, | ||
| "target": "https://collections.st-andrews.ac.uk/762345/manifest/canvas/406403" | ||
| } | ||
| ] | ||
| } | ||
| ] | ||
| } | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -229,6 +229,18 @@ export class Traverse< | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| traverseManifestItems(manifest: Manifest): Manifest { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Handle malformed manifests that have v2 @context but use v3 structure (items instead of sequences). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Convert v3 items structure to v2 sequences structure before traversal. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!manifest.sequences && (manifest as any).items && Array.isArray((manifest as any).items)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| manifest.sequences = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| '@id': `${manifest['@id']}/sequence/0`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+235
to
+237
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| manifest.sequences = [ | |
| { | |
| '@id': `${manifest['@id']}/sequence/0`, | |
| const manifestId = manifest['@id']; | |
| const sequenceId = manifestId ? `${manifestId}/sequence/0` : 'sequence/0'; | |
| manifest.sequences = [ | |
| { | |
| '@id': sequenceId, |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Directly mutating the input manifest object by setting manifest.sequences and deleting (manifest as any).items can cause side effects if the same manifest object is used elsewhere. Consider creating a shallow copy of the manifest before mutation, or document that this function mutates the input.
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The detection logic for v2-style canvases is incorrect. A canvas with @type or @id could still have v3 structure with items arrays. The check should verify the presence of v2-specific properties like images array or absence of v3-specific properties like items array, not just the presence of @type or @id.
| // If it's already v2 style, return as-is | |
| if (canvas['@type'] || canvas['@id']) { | |
| // If it's already v2 style (has images) or not clearly v3 (no items), return as-is | |
| const hasV3Items = Array.isArray((canvas as any).items); | |
| const hasV2Images = Array.isArray((canvas as any).images); | |
| if (hasV2Images || !hasV3Items) { |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code does not validate that canvas.id is defined before using it on line 264 and 277. If a v3 canvas is missing the id property, this will result in undefined values being assigned to @id properties. Add validation to handle missing IDs gracefully.
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If all annotation pages or annotations are empty (no items), this will result in an empty images array. While this is valid, it might be more appropriate to leave images undefined if there are no actual images to preserve consistency with how v2 manifests typically handle missing images arrays.
| v2Canvas.images = []; | |
| for (const annotationPage of canvas.items) { | |
| if (annotationPage.items && Array.isArray(annotationPage.items)) { | |
| for (const annotation of annotationPage.items) { | |
| v2Canvas.images.push(this.convertV3AnnotationToV2(annotation, canvas.id)); | |
| } | |
| } | |
| } | |
| const images: any[] = []; | |
| for (const annotationPage of canvas.items) { | |
| if (annotationPage.items && Array.isArray(annotationPage.items)) { | |
| for (const annotation of annotationPage.items) { | |
| images.push(this.convertV3AnnotationToV2(annotation, canvas.id)); | |
| } | |
| } | |
| } | |
| if (images.length > 0) { | |
| v2Canvas.images = images; | |
| } |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The conversion does not handle additional canvas properties like thumbnail, metadata, seeAlso, rendering, service, and other descriptive or linking properties that may be present on v3 canvases. These properties should be copied over to the v2 canvas structure to preserve all data.
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code does not validate that annotation.id is defined before using it on line 291. If a v3 annotation is missing the id property, this will result in an undefined value for the @id property. Add validation to handle missing annotation IDs.
| return { | |
| '@id': annotation.id, | |
| const hasValidId = | |
| annotation && | |
| typeof annotation.id === 'string' && | |
| annotation.id.length > 0; | |
| // Fallback: if the v3 annotation has no id, derive a stable identifier | |
| // from its target (if available) or from the canvasId to avoid an | |
| // undefined @id in the v2 annotation. | |
| const fallbackId = | |
| (annotation && typeof annotation.target === 'string' && annotation.target) || | |
| canvasId; | |
| return { | |
| '@id': hasValidId ? annotation.id : fallbackId, |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The conversion does not handle the case where annotation.body is an array of bodies. In IIIF Presentation API v3, the body property can be an array of resources. The code should check if annotation.body is an array and handle multiple bodies appropriately (e.g., by converting each or taking the first one).
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The conversion does not handle additional annotation properties like metadata, label, or stylesheet that may be present on v3 annotations. These properties should be preserved during the conversion.
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code does not validate that body.id is defined before using it on line 306. If a v3 annotation body is missing the id property, this will result in an undefined value for the @id property. Add validation to handle missing body IDs.
| return { | |
| '@id': body.id, | |
| '@type': body.type === 'Image' ? 'dctypes:Image' : `dctypes:${body.type}`, | |
| format: body.format, | |
| height: body.height, | |
| width: body.width, | |
| service: body.service, | |
| }; | |
| const id = body.id ?? body['@id']; | |
| const type = | |
| body.type != null | |
| ? body.type === 'Image' | |
| ? 'dctypes:Image' | |
| : `dctypes:${body.type}` | |
| : undefined; | |
| const resource: any = { | |
| format: body.format, | |
| height: body.height, | |
| width: body.width, | |
| service: body.service, | |
| }; | |
| if (id) { | |
| resource['@id'] = id; | |
| } | |
| if (type) { | |
| resource['@type'] = type; | |
| } | |
| return resource; |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The conversion assumes body.type is always defined, but does not handle the case where body.type is undefined or null. This could result in a malformed @type property like dctypes:undefined. Add a check to handle this case or use a default type.
| return { | |
| '@id': body.id, | |
| '@type': body.type === 'Image' ? 'dctypes:Image' : `dctypes:${body.type}`, | |
| format: body.format, | |
| height: body.height, | |
| width: body.width, | |
| service: body.service, | |
| }; | |
| const resource: any = { | |
| '@id': body.id, | |
| format: body.format, | |
| height: body.height, | |
| width: body.width, | |
| service: body.service, | |
| }; | |
| if (body.type) { | |
| resource['@type'] = | |
| body.type === 'Image' ? 'dctypes:Image' : `dctypes:${body.type}`; | |
| } | |
| return resource; |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The conversion does not handle additional body properties like label, metadata, thumbnail, seeAlso, rendering, and language that may be present on v3 annotation bodies. These properties should be preserved during the conversion to ensure no data is lost.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test does not cover edge cases such as: canvases with missing
idproperty, canvases with both v2 and v3 properties, annotations with array of bodies, annotations with missingtargetproperty, or empty annotation pages. Consider adding test cases for these scenarios to ensure robust handling.