├── .dockerignore ├── .github ├── deployment │ └── task-definition.json └── workflows │ ├── deploy.yml │ └── test.yml ├── .gitignore ├── CHANGES.md ├── Dockerfile ├── README.md ├── fixtures ├── 1 │ └── manifest.json ├── 2 │ └── manifest.json ├── 3 │ ├── accompanyingCanvas.json │ ├── annoPage.json │ ├── annoPageInCollection.json │ ├── annoPageMultipleMotivations.json │ ├── anno_pointselector.json │ ├── anno_source.json │ ├── broken_choice.json │ ├── broken_collection.json │ ├── broken_embedded_annos.json │ ├── broken_service.json │ ├── broken_simple_image.json │ ├── choice.json │ ├── collection.json │ ├── collection_of_canvases.json │ ├── collection_of_collections.json │ ├── extension_anno.json │ ├── full_example.json │ ├── multi_bodies.json │ ├── navPlace.json │ ├── non_cc_license.json │ ├── old_cc_license.json │ ├── old_format_label.json │ ├── placeholderCanvas.json │ ├── point_selector.json │ ├── publicdomain.json │ ├── range_range.json │ ├── rights_lang_issues.json │ ├── rightsstatement_license.json │ ├── simple_video.json │ └── version2image.json └── README.md ├── iiif-presentation-validator.py ├── index.html ├── requirements.txt ├── runDocker.sh ├── schema ├── __init__.py ├── error_processor.py ├── iiif_3_0.json └── schemavalidator.py ├── setup.py └── tests ├── __init__.py └── test_validator.py /.dockerignore: -------------------------------------------------------------------------------- 1 | .gitignore -------------------------------------------------------------------------------- /.github/deployment/task-definition.json: -------------------------------------------------------------------------------- 1 | { 2 | "family": "PreziValidatorTask", 3 | "placementConstraints": [], 4 | "volumes": [], 5 | "requiresCompatibilities": [ 6 | "EC2" 7 | ], 8 | "containerDefinitions": [ 9 | { 10 | "memoryReservation": 128, 11 | "environment": [], 12 | "name": "PreziValidatorContainer", 13 | "mountPoints": [], 14 | "image": "docker.io/glenrobson/iiif-presentation-validator", 15 | "cpu": 0, 16 | "portMappings": [ 17 | { 18 | "protocol": "tcp", 19 | "containerPort": 8080, 20 | "hostPort": 0 21 | } 22 | ], 23 | "essential": true, 24 | "volumesFrom": [] 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to AWS 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | 7 | jobs: 8 | 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: docker login 16 | env: 17 | DOCKER_USER: ${{secrets.DOCKER_USER}} 18 | DOCKER_PASSWORD: ${{secrets.DOCKER_PASSWORD}} 19 | run: | 20 | echo ${{ secrets.DOCKER_PASSWORD }} | docker login --username ${{ secrets.DOCKER_USER }} --password-stdin 21 | 22 | - name: Build the Docker image 23 | env: 24 | IMAGE_TAG: ${{ github.sha }} 25 | run: docker build . --file Dockerfile --tag glenrobson/iiif-presentation-validator:$IMAGE_TAG --tag glenrobson/iiif-presentation-validator:latest 26 | 27 | - name: Docker Push 28 | env: 29 | IMAGE_TAG: ${{ github.sha }} 30 | run: docker push glenrobson/iiif-presentation-validator:$IMAGE_TAG && docker push glenrobson/iiif-presentation-validator:latest 31 | 32 | - name: Docker logout 33 | run: docker logout 34 | 35 | - name: Configure AWS credentials 36 | uses: aws-actions/configure-aws-credentials@v4 37 | with: 38 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 39 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 40 | aws-region: us-east-1 41 | 42 | - name: Login to Amazon ECR 43 | id: login-ecr 44 | uses: aws-actions/amazon-ecr-login@v2 45 | 46 | - name: Fill in the new image ID in the Amazon ECS task definition 47 | id: task-def 48 | uses: aws-actions/amazon-ecs-render-task-definition@v1 49 | with: 50 | task-definition: .github/deployment/task-definition.json 51 | container-name: PreziValidatorContainer 52 | image: docker.io/glenrobson/iiif-presentation-validator:${{ github.sha }} 53 | 54 | - name: Deploy Amazon ECS task definition 55 | uses: aws-actions/amazon-ecs-deploy-task-definition@v2 56 | with: 57 | task-definition: ${{ steps.task-def.outputs.task-definition }} 58 | service: PreziValidatorService 59 | cluster: DockerHost 60 | wait-for-service-stability: true 61 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: Run-tests 4 | 5 | # Controls when the action will run. Triggers the workflow on push or pull request 6 | # events but only for the master branch 7 | on: [push] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | python-version: [ '3.9', '3.10', '3.11'] 15 | name: Python ${{ matrix.python-version }} sample 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Setup python 19 | uses: actions/setup-python@v5 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | architecture: x64 23 | 24 | - uses: actions/cache@v4 25 | with: 26 | path: ${{ env.pythonLocation }} 27 | key: ${{ env.pythonLocation }}-${{ hashFiles('setup.py') }}-${{ hashFiles('dev-requirements.txt') }} 28 | 29 | - name: Install setup tools 30 | run: pip install setuptools 31 | 32 | - name: Install 33 | run: python setup.py install 34 | 35 | - name: Test 36 | run: python setup.py test 37 | 38 | - name: install coveralls 39 | run: pip install coveralls 40 | 41 | 42 | - name: Generate coverage 43 | run: coverage run --include=iiif-presentation-validator.py setup.py test 44 | 45 | - name: Upload coverage data to coveralls.io 46 | run: coveralls --service=github 47 | env: 48 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 49 | COVERALLS_FLAG_NAME: ${{ matrix.python-version }} 50 | COVERALLS_PARALLEL: true 51 | 52 | Coveralls: 53 | needs: build 54 | runs-on: ubuntu-latest 55 | container: python:3-slim 56 | steps: 57 | - name: Coveralls Finished 58 | run: | 59 | pip3 install --upgrade coveralls 60 | coveralls --service=github --finish 61 | env: 62 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .c 3 | .coverage 4 | *.egg 5 | *.egg-info 6 | .eggs 7 | build 8 | __pycache__ 9 | htmlcov 10 | *.pyc 11 | *.swp 12 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # `presentation-validator` change log 2 | 3 | v0.0.1 2016-10-27 4 | * Refactored into own repository. 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG version=3.9 2 | FROM python:${version}-slim 3 | 4 | 5 | WORKDIR /app 6 | ADD . /app 7 | 8 | RUN pip install --trusted-host pypi.python.org -r requirements.txt 9 | 10 | EXPOSE 8080 11 | CMD ["/usr/local/bin/python", "/app/iiif-presentation-validator.py", "--hostname", "0.0.0.0"] 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | IIIF Presentation Validator 2 | ====================== 3 | 4 | This is the codebase for the IIIF Presentation Validator, which can be seen at . 5 | 6 | ## Usage 7 | 8 | *(Write me)* 9 | 10 | ## Data Structure 11 | 12 | **JSON Response** 13 | 14 | ```JSON 15 | { 16 | "url": "", 17 | "error": "", 18 | "okay": 1, 19 | "warnings": [] 20 | } 21 | ``` 22 | 23 | Key | Definition | Example Value 24 | ---------|-----------------------------------|---------- 25 | url | Submitted URL for the manifest | http://example.com/iiif/manifest.json 26 | error | The text of the breaking error | sc:Manifest['thumbnail'] has broken value 27 | okay | Did the manifest parse properly? | 1 *or* 0 28 | warnings | An array of warning messages | "WARNING: Resource type 'sc:Manifest' should have 'description' set\n" 29 | 30 | ## Local Installation 31 | 32 | 33 | **Step one: Install dependencies** 34 | 35 | ```bash 36 | python setup.py install 37 | ``` 38 | 39 | **Step two: Run the application** 40 | 41 | ```bash 42 | python iiif-presentation-validator.py 43 | ``` 44 | 45 | This should start up a local server, running at . To test it, try [this url](http://localhost:8080/validate?url=http://iiif.io/api/presentation/2.1/example/fixtures/1/manifest.json) and see if you get a JSON response that looks like this: 46 | 47 | ```json 48 | { 49 | "url": "http://iiif.io/api/presentation/2.1/example/fixtures/1/manifest.json", 50 | "error": "None", 51 | "okay": 1, 52 | "warnings": ["WARNING: Resource type 'sc:Manifest' should have 'description' set\n", "WARNING: Resource type 'sc:Sequence' should have '@id' set\n", "WARNING: Resource type 'oa:Annotation' should have '@id' set\n", "WARNING: Resource type 'dctypes:Image' should have 'format' set\n"] 53 | } 54 | ``` 55 | You may also use `--hostname` to specify a hostname or IP address to which to bind and `--port` for a port to which to bind. 56 | -------------------------------------------------------------------------------- /fixtures/1/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "http://iiif.io/api/presentation/2/context.json", 3 | "@id": "http://iiif.io/api/presentation/2.0/example/fixtures/1/manifest.json", 4 | "@type": "sc:Manifest", 5 | "label": "Test 1 Manifest: Minimum Required Fields", 6 | "within": "http://iiif.io/api/presentation/2.0/example/fixtures/collection.json", 7 | "sequences": [ 8 | { 9 | "@type": "sc:Sequence", 10 | "canvases": [ 11 | { 12 | "@id": "http://iiif.io/api/presentation/2.0/example/fixtures/canvas/1/c1.json", 13 | "@type": "sc:Canvas", 14 | "label": "Test 1 Canvas: 1", 15 | "height": 1800, 16 | "width": 1200, 17 | "images": [ 18 | { 19 | "@type": "oa:Annotation", 20 | "motivation": "sc:painting", 21 | "resource": { 22 | "@id": "http://iiif.io/api/presentation/2.0/example/fixtures/resources/page1-full.png", 23 | "@type": "dctypes:Image", 24 | "height": 1800, 25 | "width": 1200 26 | }, 27 | "on": "http://iiif.io/api/presentation/2.0/example/fixtures/canvas/1/c1.json" 28 | } 29 | ] 30 | } 31 | ] 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /fixtures/2/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "http://iiif.io/api/presentation/2/context.json", 3 | "@id": "http://iiif.io/api/presentation/2.0/example/fixtures/2/manifest.json", 4 | "@type": "sc:Manifest", 5 | "label": "Test 2 Manifest: Metadata Pairs", 6 | "metadata": [ 7 | { 8 | "label": "date", 9 | "value": "some date" 10 | } 11 | ], 12 | "within": "http://iiif.io/api/presentation/2.0/example/fixtures/collection.json", 13 | "sequences": [ 14 | { 15 | "@type": "sc:Sequence", 16 | "label": "Test 2 Sequence 1", 17 | "canvases": [ 18 | { 19 | "@id": "http://iiif.io/api/presentation/2.0/example/fixtures/canvas/2/c1.json", 20 | "@type": "sc:Canvas", 21 | "label": "Test 2 Canvas: 1", 22 | "height": 1800, 23 | "width": 1200, 24 | "images": [ 25 | { 26 | "@type": "oa:Annotation", 27 | "motivation": "sc:painting", 28 | "resource": { 29 | "@id": "http://iiif.io/api/presentation/2.0/example/fixtures/resources/page1-full.png", 30 | "@type": "dctypes:Image", 31 | "height": 1800, 32 | "width": 1200 33 | }, 34 | "on": "http://iiif.io/api/presentation/2.0/example/fixtures/canvas/2/c1.json" 35 | } 36 | ] 37 | } 38 | ] 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /fixtures/3/accompanyingCanvas.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "http://iiif.io/api/presentation/3/context.json", 3 | "id": "https://iiif.io/api/cookbook/recipe/0014-accompanyingcanvas/manifest.json", 4 | "type": "Manifest", 5 | "label": { 6 | "en": [ 7 | "Partial audio recording of Gustav Mahler's _Symphony No. 3_" 8 | ] 9 | }, 10 | "items": [ 11 | { 12 | "id": "https://iiif.io/api/cookbook/recipe/0014-accompanyingcanvas/canvas/p1", 13 | "type": "Canvas", 14 | "label": { 15 | "en": [ 16 | "Gustav Mahler, Symphony No. 3, CD 1" 17 | ] 18 | }, 19 | "duration": 1985.024, 20 | "accompanyingCanvas": { 21 | "id": "https://iiif.io/api/cookbook/recipe/0014-accompanyingcanvas/canvas/accompanying", 22 | "type": "Canvas", 23 | "label": { 24 | "en": [ 25 | "First page of score for Gustav Mahler, Symphony No. 3" 26 | ] 27 | }, 28 | "height": 998, 29 | "width": 772, 30 | "items": [ 31 | { 32 | "id": "https://iiif.io/api/cookbook/recipe/0014-accompanyingcanvas/canvas/accompanying/annotation/page", 33 | "type": "AnnotationPage", 34 | "items": [ 35 | { 36 | "id": "https://iiif.io/api/cookbook/recipe/0014-accompanyingcanvas/canvas/accompanying/annotation/image", 37 | "type": "Annotation", 38 | "motivation": "painting", 39 | "body": { 40 | "id": "https://iiif.io/api/image/3.0/example/reference/4b45bba3ea612ee46f5371ce84dbcd89-mahler-0/full/,998/0/default.jpg", 41 | "type": "Image", 42 | "format": "image/jpeg", 43 | "height": 998, 44 | "width": 772, 45 | "service": [ 46 | { 47 | "id": "https://iiif.io/api/image/3.0/example/reference/4b45bba3ea612ee46f5371ce84dbcd89-mahler-0", 48 | "type": "ImageService3", 49 | "profile": "level1" 50 | } 51 | ] 52 | }, 53 | "target": "https://iiif.io/api/cookbook/recipe/0014-accompanyingcanvas/canvas/accompanying" 54 | } 55 | ] 56 | } 57 | ] 58 | }, 59 | "items": [ 60 | { 61 | "id": "https://iiif.io/api/cookbook/recipe/0014-accompanyingcanvas/canvas/page/p1", 62 | "type": "AnnotationPage", 63 | "items": [ 64 | { 65 | "id": "https://iiif.io/api/cookbook/recipe/0014-accompanyingcanvas/canvas/page/annotation/segment1-audio", 66 | "type": "Annotation", 67 | "motivation": "painting", 68 | "body": { 69 | "id": "https://fixtures.iiif.io/audio/indiana/mahler-symphony-3/CD1/medium/128Kbps.mp4", 70 | "type": "Sound", 71 | "duration": 1985.024, 72 | "format": "video/mp4" 73 | }, 74 | "target": "https://iiif.io/api/cookbook/recipe/0014-accompanyingcanvas/canvas/page/p1" 75 | } 76 | ] 77 | } 78 | ] 79 | } 80 | ] 81 | } -------------------------------------------------------------------------------- /fixtures/3/annoPage.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "http://iiif.io/api/presentation/3/context.json", 3 | "id": "https://preview.iiif.io/cookbook/0068-newspaper/recipe/0068-newspaper/newspaper_issue_1-anno_p1.json", 4 | "type": "AnnotationPage", 5 | "items": [ 6 | { 7 | "id": "https://data.europeana.eu/annotation/9200355/BibliographicResource_3000096302513/20b3b1f4cb15f062e53fd50d584d66ff", 8 | "type": "Annotation", 9 | "motivation": "supplementing", 10 | "body": { 11 | "type": "TextualBody", 12 | "format": "text/plain", 13 | "language": "de", 14 | "value": "84" 15 | }, 16 | "target": "https://iiif.europeana.eu/presentation/9200355/BibliographicResource_3000096302513/canvas/p1#xywh=182,476,59,43" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /fixtures/3/annoPageInCollection.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "http://iiif.io/api/presentation/3/context.json", 3 | "id": "https://preview.iiif.io/cookbook/309-annotation-collections/recipe/0309-annotation-collection/anno_p1.json", 4 | "type": "AnnotationPage", 5 | "partOf": [{ 6 | "id": "https://preview.iiif.io/cookbook/309-annotation-collections/recipe/0309-annotation-collection/anno_coll.json", 7 | "type": "AnnotationCollection" 8 | }], 9 | "next": { 10 | "id": "https://preview.iiif.io/cookbook/309-annotation-collections/recipe/0309-annotation-collection/anno_p2.json", 11 | "type": "AnnotationPage" 12 | }, 13 | "items": [ 14 | { 15 | "id": "https://preview.iiif.io/cookbook/309-annotation-collections/recipe/0309-annotation-collection/anno_p1.json-1", 16 | "type": "Annotation", 17 | "motivation": "tagging", 18 | "body": { 19 | "type": "TextualBody", 20 | "format": "text/plain", 21 | "value": "text-1-1" 22 | }, 23 | "target": { 24 | "type": "SpecificResource", 25 | "source": { 26 | "id": "https://preview.iiif.io/cookbook/309-annotation-collections/recipe/0309-annotation-collection/canvas/p1", 27 | "type": "Canvas", 28 | "partOf": [{ 29 | "id": "https://preview.iiif.io/cookbook/309-annotation-collections/recipe/0309-annotation-collection/manifest.json", 30 | "type": "Manifest" 31 | }] 32 | }, 33 | "selector": { 34 | "type": "FragmentSelector", 35 | "conformsTo": "http://www.w3.org/TR/media-frags/", 36 | "value": "xywh=88,957,2768,248" 37 | } 38 | } 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /fixtures/3/annoPageMultipleMotivations.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "http://iiif.io/api/presentation/3/context.json", 3 | "id": "https://preview.iiif.io/cookbook/0068-newspaper/recipe/0068-newspaper/newspaper_issue_1-anno_p1.json", 4 | "type": "AnnotationPage", 5 | "items": [ 6 | { 7 | "id": "https://data.europeana.eu/annotation/9200355/BibliographicResource_3000096302513/20b3b1f4cb15f062e53fd50d584d66ff", 8 | "type": "Annotation", 9 | "motivation": ["supplementing", "commenting"], 10 | "body": { 11 | "type": "TextualBody", 12 | "format": "text/plain", 13 | "language": "de", 14 | "value": "84" 15 | }, 16 | "target": "https://iiif.europeana.eu/presentation/9200355/BibliographicResource_3000096302513/canvas/p1#xywh=182,476,59,43" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /fixtures/3/anno_pointselector.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "http://iiif.io/api/presentation/3/context.json", 3 | "id": "https://preview.iiif.io/cookbook/0103-poetry/recipe/0103-poetry-reading-annotations/annotations.json", 4 | "type": "AnnotationPage", 5 | "items": [ 6 | { 7 | "id": "https://preview.iiif.io/cookbook/0103-poetry/recipe/0103-poetry-reading-annotations/canvas/annotation1", 8 | "type": "Annotation", 9 | "motivation": "commenting", 10 | "body": { 11 | "type": "TextualBody", 12 | "value": "breath", 13 | "format": "text/plain" 14 | }, 15 | "target": { 16 | "type": "SpecificResource", 17 | "source": "https://preview.iiif.io/cookbook/0103-poetry/recipe/0103-poetry-reading-annotations/canvas/1", 18 | "selector": { 19 | "type": "PointSelector", 20 | "t": 27.660653 21 | } 22 | } 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /fixtures/3/anno_source.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "http://iiif.io/api/presentation/3/context.json", 3 | "id": "https://preview.iiif.io/cookbook/0326-annotating-image-layer/recipe/0326-annotating-image-layer/manifest.json", 4 | "type": "Manifest", 5 | "label": { 6 | "en": [ 7 | "Choice Example with layer specific annotation" 8 | ] 9 | }, 10 | "items": [ 11 | { 12 | "id": "https://preview.iiif.io/cookbook/0326-annotating-image-layer/recipe/0326-annotating-image-layer/canvas/p1", 13 | "type": "Canvas", 14 | "height": 1271, 15 | "width": 2000, 16 | "items": [ 17 | { 18 | "id": "https://preview.iiif.io/cookbook/0326-annotating-image-layer/recipe/0326-annotating-image-layer/page/p1/1", 19 | "type": "AnnotationPage", 20 | "items": [ 21 | { 22 | "id": "https://preview.iiif.io/cookbook/0326-annotating-image-layer/recipe/0326-annotating-image-layer/annotation/p0001-image", 23 | "type": "Annotation", 24 | "motivation": "painting", 25 | "body": { 26 | "type": "Choice", 27 | "items": [ 28 | { 29 | "id": "https://iiif.io/api/image/3.0/example/reference/421e65be2ce95439b3ad6ef1f2ab87a9-dee-natural/full/max/0/default.jpg", 30 | "type": "Image", 31 | "label": { 32 | "en": [ 33 | "Natural Light" 34 | ] 35 | }, 36 | "format": "image/jpeg", 37 | "height": 1271, 38 | "width": 2000, 39 | "service": [ 40 | { 41 | "id": "https://iiif.io/api/image/3.0/example/reference/421e65be2ce95439b3ad6ef1f2ab87a9-dee-natural", 42 | "type": "ImageService3", 43 | "profile": "level1" 44 | } 45 | ] 46 | }, 47 | { 48 | "id": "https://iiif.io/api/image/3.0/example/reference/421e65be2ce95439b3ad6ef1f2ab87a9-dee-xray/full/max/0/default.jpg", 49 | "type": "Image", 50 | "label": { 51 | "en": [ 52 | "X-Ray" 53 | ] 54 | }, 55 | "format": "image/jpeg", 56 | "height": 1271, 57 | "width": 2000, 58 | "service": [ 59 | { 60 | "id": "https://iiif.io/api/image/3.0/example/reference/421e65be2ce95439b3ad6ef1f2ab87a9-dee-xray", 61 | "type": "ImageService3", 62 | "profile": "level1" 63 | } 64 | ], 65 | "annotations": [ 66 | { 67 | "id": "https://preview.iiif.io/cookbook/0326-annotating-image-layer/recipe/0326-annotating-image-layer/page/p2/1", 68 | "type": "AnnotationPage", 69 | "items": [ 70 | { 71 | "id": "https://preview.iiif.io/cookbook/0326-annotating-image-layer/recipe/0326-annotating-image-layer/annotation/p0002-tag", 72 | "type": "Annotation", 73 | "motivation": "tagging", 74 | "body": { 75 | "type": "TextualBody", 76 | "value": "A group of skulls.", 77 | "language": "en", 78 | "format": "text/plain" 79 | }, 80 | "target": { 81 | "type":"SpecificResource", 82 | "source": "https://iiif.io/api/image/3.0/example/reference/421e65be2ce95439b3ad6ef1f2ab87a9-dee-xray/full/max/0/default.jpg#xywh=810,900,260,370", 83 | "scope": "https://preview.iiif.io/cookbook/0326-annotating-image-layer/recipe/0326-annotating-image-layer/canvas/p1" 84 | } 85 | } 86 | ] 87 | } 88 | ] 89 | } 90 | ] 91 | }, 92 | "target": "https://preview.iiif.io/cookbook/0326-annotating-image-layer/recipe/0326-annotating-image-layer/canvas/p1" 93 | } 94 | ] 95 | } 96 | ] 97 | } 98 | ] 99 | } 100 | -------------------------------------------------------------------------------- /fixtures/3/broken_choice.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": [ 3 | "http://www.w3.org/ns/anno.jsonld", 4 | "http://iiif.io/api/presentation/3/context.json" 5 | ], 6 | "type": "Manifest", 7 | "id": "https://iiif.gdmrdigital.com/nlw/LLOYD_GEORGE_FARMING-mp4.json", 8 | "label": { 9 | "en": [ 10 | "Lloyd George: farming, officiating and breakfasting" 11 | ] 12 | }, 13 | "metadata": [ 14 | { 15 | "label": { 16 | "en": [ 17 | "Title" 18 | ] 19 | }, 20 | "value": { 21 | "en": [ 22 | "Lloyd George: farming, officiating and breakfasting" 23 | ] 24 | } 25 | } 26 | ], 27 | "items": [ 28 | { 29 | "type": "Canvas", 30 | "id": "https://iiif.gdmrdigital.com/nlw/LLOYD_GEORGE_FARMING/canvas/1", 31 | "thumbnail": [ 32 | { 33 | "id": "https://datasyllwr.llgc.org.uk/video/dlg/LLOYD_GEORGE_FARMING.jpg", 34 | "type": "Image" 35 | } 36 | ], 37 | "items": [ 38 | { 39 | "type": "AnnotationPage", 40 | "id": "https://iiif.gdmrdigital.com/nlw/LLOYD_GEORGE_FARMING/AnnotationPage/1", 41 | "items": [ 42 | { 43 | "id": "https://iiif.gdmrdigital.com/nlw/LLOYD_GEORGE_FARMING/Annotation/1", 44 | "type": "Annotation", 45 | "motivation": "painting", 46 | "target": "https://iiif.gdmrdigital.com/nlw/LLOYD_GEORGE_FARMING/canvas/1", 47 | "body": { 48 | "type": "Choice", 49 | "items": [ 50 | { 51 | "id": "https://datasyllwr.llgc.org.uk/video/dlg/LLOYD_GEORGE_FARMING.mp4", 52 | "type": "Video", 53 | "height": 360, 54 | "width": 480, 55 | "duration": 765, 56 | "format": "video/mp4" 57 | }, 58 | { 59 | "id": "https://datasyllwr.llgc.org.uk/video/dlg/LLOYD_GEORGE_FARMING.avi", 60 | "type": "Video", 61 | "format": "video/mp4" 62 | } 63 | ] 64 | } 65 | } 66 | ] 67 | } 68 | ], 69 | "width": 480 70 | } 71 | ] 72 | } 73 | -------------------------------------------------------------------------------- /fixtures/3/broken_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "http://iiif.io/api/presentation/3/context.json", 3 | "id": "https://example.org/iiif/collection/top", 4 | "type": "Collection", 5 | "summary": { "en": [ "Short summary of the Collection" ] }, 6 | "requiredStatement": { 7 | "label": { "en": [ "Attribution" ] }, 8 | "value": { "en": [ "Provided by Example Organization" ] } 9 | }, 10 | "behavior": [ "multi-part" ], 11 | "items": [ 12 | { 13 | "id": "https://example.org/iiif/1/manifest", 14 | "type": "Manifest", 15 | "label": { "en": [ "Example Manifest 1" ] }, 16 | "thumbnail": [ 17 | { 18 | "id": "https://example.org/manifest1/thumbnail.jpg", 19 | "type": "Image", 20 | "format": "image/jpeg" 21 | } 22 | ] 23 | }, 24 | { 25 | "id": "https://example.org/iiif/2/manifest", 26 | "type": "Manifest", 27 | "label": { "en": [ "Example Manifest 2" ] }, 28 | "thumbnail": [ 29 | { 30 | "id": "https://example.org/manifest2/thumbnail.jpg", 31 | "type": "Image", 32 | "format": "image/jpeg" 33 | } 34 | ] 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /fixtures/3/broken_embedded_annos.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "http://iiif.io/api/presentation/3/context.json", 3 | "id": "https://example.com/broken_embedded_annos.json", 4 | "type": "Manifest", 5 | "label": { 6 | "en": [ 7 | "Audio Recording annotation with annotations in Canvas items rather than canvas/annotations" 8 | ] 9 | }, 10 | "items": [ 11 | { 12 | "id": "https://example.com/annos/canvas/1", 13 | "type": "Canvas", 14 | "duration": 107, 15 | "items": [ 16 | { 17 | "id": "https://example.com/annos/canvas/1/paintings", 18 | "type": "AnnotationPage", 19 | "items": [ 20 | { 21 | "id": "https://example.com/annos/canvas/1/painting/1", 22 | "type": "Annotation", 23 | "motivation": "painting", 24 | "body": { 25 | "id": "https://library.harvard.edu/poetry/audio/listeningbooth/PS3537E915A6x1974/Her_Kind.mp3", 26 | "type": "Sound", 27 | "format": "audio/mp3", 28 | "duration": 107 29 | }, 30 | "target": "https://example.com/annos/canvas/1" 31 | } 32 | ], 33 | "annotations": [ 34 | { 35 | "id": "https://example.com/annos/annotations.json", 36 | "type": "AnnotationPage", 37 | "items": [ 38 | { 39 | "@context": "http://www.w3.org/ns/anno.jsonld", 40 | "id": "https://example.com/annos/canvas/1/annotation/1", 41 | "type": "Annotation", 42 | "motivation": "commenting", 43 | "body": { 44 | "type": "TextualBody", 45 | "value": "breath", 46 | "format": "text/plain" 47 | }, 48 | "target": { 49 | "source": "https://example.com/annos/canvas/1", 50 | "selector": { 51 | "type": "PointSelector", 52 | "t": "27.660653" 53 | } 54 | } 55 | }, 56 | { 57 | "@context": "http://www.w3.org/ns/anno.jsonld", 58 | "id": "https://example.com/annos/canvas/1/annotation/2", 59 | "type": "Annotation", 60 | "motivation": "commenting", 61 | "body": { 62 | "type": "TextualBody", 63 | "value": "her kind", 64 | "format": "text/plain" 65 | }, 66 | "target": { 67 | "source": "https://example.com/annos/canvas/1", 68 | "selector": { 69 | "type": "RangeSelector", 70 | "t": "46.734653,47.875068" 71 | } 72 | } 73 | } 74 | ] 75 | } 76 | ] 77 | } 78 | ] 79 | } 80 | ] 81 | } 82 | -------------------------------------------------------------------------------- /fixtures/3/broken_service.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "http://iiif.io/api/presentation/3/context.json", 3 | "id": "https://example.org/iiif/book1/manifest", 4 | "type": "Manifest", 5 | "label": { "en": [ "Book 1" ] }, 6 | "thumbnail": [ 7 | { 8 | "id": "https://example.org/iiif/book1/page1/full/80,100/0/default.jpg", 9 | "type": "Image", 10 | "format": "image/jpeg", 11 | "service": 12 | { 13 | "id": "https://example.org/iiif/book1/page1", 14 | "type": "ImageService3", 15 | "profile": "level1" 16 | } 17 | 18 | } 19 | ], 20 | "items": [ 21 | { 22 | "id": "https://example.org/iiif/book1/canvas/p1", 23 | "type": "Canvas", 24 | "height": 1000, 25 | "width": 750, 26 | "items": [ 27 | { 28 | "id": "https://example.org/iiif/book1/page/p1/1", 29 | "type": "AnnotationPage", 30 | "items": [ 31 | { 32 | "id": "https://example.org/iiif/book1/annotation/p0001-image", 33 | "type": "Annotation", 34 | "motivation": "painting", 35 | "body": { 36 | "id": "https://example.org/iiif/book1/page1/full/max/0/default.jpg", 37 | "type": "Image", 38 | "format": "image/jpeg", 39 | "service": 40 | { 41 | "id": "https://example.org/iiif/book1/page1", 42 | "type": "ImageService3", 43 | "profile": "level2" 44 | } 45 | , 46 | "height": 2000, 47 | "width": 1500 48 | }, 49 | "target": "https://example.org/iiif/book1/canvas/p1" 50 | } 51 | ] 52 | } 53 | ] 54 | } 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /fixtures/3/broken_simple_image.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": [ 3 | "http://www.w3.org/ns/anno.jsonld", 4 | "http://iiif.io/api/presentation/{{ page.major }}/context.json" 5 | ], 6 | "id": "http://example.org/iiif/book1/manifest", 7 | "type": "Manifest", 8 | "label": { 9 | "en": [ "Image 1" ], 10 | "zh-Hant-CN": ["test2", "Test3"], 11 | "none": ["test3"], 12 | "cy": ["test"] 13 | }, 14 | "provider": [ 15 | { 16 | "id": "https://example.org/about", 17 | "type": "Agent", 18 | "label": { "en": [ "Example Organization" ] }, 19 | "homepage": [ 20 | { 21 | "id": "https://example.org/", 22 | "type": "Text", 23 | "label": { "en": [ "Example Organization Homepage" ] }, 24 | "format": "text/html" 25 | } 26 | ], 27 | "logo": [ 28 | { 29 | "type": "Image", 30 | "format": "image/png", 31 | "height": 100, 32 | "width": 120, 33 | "service": [ 34 | { 35 | "id": "https://example.org/service/inst1", 36 | "type": "ImageService3", 37 | "profile": "level2" 38 | } 39 | ] 40 | } 41 | ], 42 | "seeAlso": [ 43 | { 44 | "type": "Dataset", 45 | "format": "application/ld+json", 46 | "profile": "https://schema.org/" 47 | } 48 | ] 49 | } 50 | ], 51 | "items": [ 52 | { 53 | "type": "Canvas", 54 | "height": 1800, 55 | "width": 1200, 56 | "items": [ 57 | { 58 | "id": "https://example.org/iiif/book1/page/p1/1", 59 | "type": "AnnotationPage", 60 | "items": [ 61 | { 62 | "id": "https://example.org/iiif/book1/annotation/p0001-image", 63 | "type": "Annotation", 64 | "motivation": "painting", 65 | "body": { 66 | "id": "http://iiif.io/api/presentation/2.1/example/fixtures/resources/page1-full.png", 67 | "type": "Image", 68 | "format": "image/png", 69 | "height": 1800, 70 | "width": 1200 71 | }, 72 | "target": "https://example.org/iiif/book1/canvas/p1" 73 | } 74 | ] 75 | } 76 | ] 77 | } 78 | ] 79 | } 80 | -------------------------------------------------------------------------------- /fixtures/3/choice.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": [ 3 | "http://www.w3.org/ns/anno.jsonld", 4 | "http://iiif.io/api/presentation/3/context.json" 5 | ], 6 | "type": "Manifest", 7 | "id": "https://iiif.gdmrdigital.com/nlw/LLOYD_GEORGE_FARMING-mp4.json", 8 | "label": { 9 | "en": [ 10 | "Lloyd George: farming, officiating and breakfasting" 11 | ] 12 | }, 13 | "metadata": [ 14 | { 15 | "label": { 16 | "en": [ 17 | "Title" 18 | ] 19 | }, 20 | "value": { 21 | "en": [ 22 | "Lloyd George: farming, officiating and breakfasting" 23 | ] 24 | } 25 | } 26 | ], 27 | "items": [ 28 | { 29 | "type": "Canvas", 30 | "id": "https://iiif.gdmrdigital.com/nlw/LLOYD_GEORGE_FARMING/canvas/1", 31 | "thumbnail": [ 32 | { 33 | "id": "https://datasyllwr.llgc.org.uk/video/dlg/LLOYD_GEORGE_FARMING.jpg", 34 | "type": "Image" 35 | } 36 | ], 37 | "items": [ 38 | { 39 | "type": "AnnotationPage", 40 | "id": "https://iiif.gdmrdigital.com/nlw/LLOYD_GEORGE_FARMING/AnnotationPage/1", 41 | "items": [ 42 | { 43 | "id": "https://iiif.gdmrdigital.com/nlw/LLOYD_GEORGE_FARMING/Annotation/1", 44 | "type": "Annotation", 45 | "motivation": "painting", 46 | "target": "https://iiif.gdmrdigital.com/nlw/LLOYD_GEORGE_FARMING/canvas/1", 47 | "body": { 48 | "type": "Choice", 49 | "items": [ 50 | { 51 | "id": "https://datasyllwr.llgc.org.uk/video/dlg/LLOYD_GEORGE_FARMING.mp4", 52 | "type": "Video", 53 | "height": 360, 54 | "width": 480, 55 | "duration": 765, 56 | "format": "video/mp4" 57 | }, 58 | { 59 | "id": "https://datasyllwr.llgc.org.uk/video/dlg/LLOYD_GEORGE_FARMING.avi", 60 | "type": "Video", 61 | "format": "video/mp4" 62 | } 63 | ] 64 | } 65 | } 66 | ] 67 | } 68 | ], 69 | "width": 480, 70 | "height": 360, 71 | "duration": 765 72 | } 73 | ] 74 | } 75 | -------------------------------------------------------------------------------- /fixtures/3/collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "http://iiif.io/api/presentation/3/context.json", 3 | "id": "https://example.org/iiif/collection/top", 4 | "type": "Collection", 5 | "label": { "en": [ "Collection for Example Organization" ] }, 6 | "summary": { "en": [ "Short summary of the Collection" ] }, 7 | "requiredStatement": { 8 | "label": { "en": [ "Attribution" ] }, 9 | "value": { "en": [ "Provided by Example Organization" ] } 10 | }, 11 | "behavior": [ "multi-part" ], 12 | "items": [ 13 | { 14 | "id": "https://example.org/iiif/1/manifest", 15 | "type": "Manifest", 16 | "label": { "en": [ "Example Manifest 1" ] }, 17 | "thumbnail": [ 18 | { 19 | "id": "https://example.org/manifest1/thumbnail.jpg", 20 | "type": "Image", 21 | "format": "image/jpeg" 22 | } 23 | ] 24 | }, 25 | { 26 | "id": "https://example.org/iiif/2/manifest", 27 | "type": "Manifest", 28 | "label": { "en": [ "Example Manifest 2" ] }, 29 | "thumbnail": [ 30 | { 31 | "id": "https://example.org/manifest2/thumbnail.jpg", 32 | "type": "Image", 33 | "format": "image/jpeg" 34 | } 35 | ] 36 | } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /fixtures/3/collection_of_canvases.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "http://iiif.io/api/presentation/3/context.json", 3 | "id": "https://example.org/iiif/collection/invalid", 4 | "type": "Collection", 5 | "label": { "en": [ "Invalid collection of canvases and ranges" ] }, 6 | "items": [ 7 | { 8 | "id": "https://example.org/iiif/canvas/1", 9 | "type": "Canvas", 10 | "label": { "en": [ "Canvas 1" ] }, 11 | "thumbnail": [ 12 | { 13 | "id": "https://example.org/manifest1/thumbnail.jpg", 14 | "type": "Image", 15 | "format": "image/jpeg" 16 | } 17 | ] 18 | }, 19 | { 20 | "id": "https://example.org/iiif/2/range/1", 21 | "type": "Range", 22 | "label": { "en": [ "Range example" ] }, 23 | "thumbnail": [ 24 | { 25 | "id": "https://example.org/manifest2/thumbnail.jpg", 26 | "type": "Image", 27 | "format": "image/jpeg" 28 | } 29 | ] 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /fixtures/3/collection_of_collections.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "http://iiif.io/api/presentation/3/context.json", 3 | "id": "https://example.org/iiif/collection/top", 4 | "type": "Collection", 5 | "label": { "en": [ "Collection of Collections!" ] }, 6 | "summary": { "en": [ "Short summary of the Collection" ] }, 7 | "requiredStatement": { 8 | "label": { "en": [ "Attribution" ] }, 9 | "value": { "en": [ "Provided by Example Organization" ] } 10 | }, 11 | "behavior": [ "multi-part" ], 12 | "items": [ 13 | { 14 | "id": "https://example.org/iiif/collection/sub1", 15 | "type": "Collection", 16 | "label": { "en": [ "Sub Collection 1" ] }, 17 | "thumbnail": [ 18 | { 19 | "id": "https://example.org/manifest1/thumbnail.jpg", 20 | "type": "Image", 21 | "format": "image/jpeg" 22 | } 23 | ], 24 | "items": [ 25 | { 26 | "id": "https://example.org/iiif/1/manifest", 27 | "type": "Manifest", 28 | "label": { "en": [ "Example Manifest 1" ] }, 29 | "thumbnail": [ 30 | { 31 | "id": "https://example.org/manifest1/thumbnail.jpg", 32 | "type": "Image", 33 | "format": "image/jpeg" 34 | } 35 | ] 36 | }, 37 | { 38 | "id": "https://example.org/iiif/2/manifest", 39 | "type": "Manifest", 40 | "label": { "en": [ "Example Manifest 2" ] }, 41 | "thumbnail": [ 42 | { 43 | "id": "https://example.org/manifest2/thumbnail.jpg", 44 | "type": "Image", 45 | "format": "image/jpeg" 46 | } 47 | ] 48 | } 49 | ] 50 | }, 51 | { 52 | "id": "https://example.org/iiif/2/collection/sub2", 53 | "type": "Collection", 54 | "label": { "en": [ "Sub Collection 2" ] }, 55 | "thumbnail": [ 56 | { 57 | "id": "https://example.org/manifest2/thumbnail.jpg", 58 | "type": "Image", 59 | "format": "image/jpeg" 60 | } 61 | ] 62 | } 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /fixtures/3/extension_anno.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": [ 3 | "http://geojson.org/geojson-ld/geojson-context.jsonld", 4 | "http://iiif.io/api/presentation/3/context.json" 5 | ], 6 | "id": "http://localhost:4000/recipe/0139-geo-annotation/manifest.json", 7 | "type": "Manifest", 8 | "label": { 9 | "en": [ 10 | "Geographic Annotation Example Manifest" 11 | ] 12 | }, 13 | "summary": { 14 | "en": [ 15 | "A Manifest containing GeoJSON-LD Web Annotations to target the word 'Paris' on the Cover Page of Le Scarabée D'or in order to transcribe it and geocode it to Paris, France." 16 | ] 17 | }, 18 | "items": [ 19 | { 20 | "id": "http://localhost:4000/recipe/0139-geo-annotation/canvas.json", 21 | "type": "Canvas", 22 | "label": { 23 | "fr": [ 24 | "Page de Couverture du Le Scarabée D'or" 25 | ] 26 | }, 27 | "width": 3204, 28 | "height": 4613, 29 | "items": [ 30 | { 31 | "id": "http://localhost:4000/recipe/0139-geo-annotation/contentPage.json", 32 | "type": "AnnotationPage", 33 | "items": [ 34 | { 35 | "id": "http://localhost:4000/recipe/0139-geo-annotation/content.json", 36 | "type": "Annotation", 37 | "motivation": "painting", 38 | "label": { 39 | "en": [ 40 | "The cover page." 41 | ] 42 | }, 43 | "body": { 44 | "id": "https://iiif.io/api/image/3.0/example/reference/59d09e6773341f28ea166e9f3c1e674f-gallica_ark_12148_bpt6k1526005v_f20/full/max/0/default.jpg", 45 | "type": "Image", 46 | "format": "image/jpeg", 47 | "service": [ 48 | { 49 | "id": "https://iiif.io/api/image/3.0/example/reference/59d09e6773341f28ea166e9f3c1e674f-gallica_ark_12148_bpt6k1526005v_f20", 50 | "type": "ImageService3", 51 | "profile": "level1" 52 | } 53 | ], 54 | "width": 3204, 55 | "height": 4613 56 | }, 57 | "target": "http://localhost:4000/recipe/0139-geo-annotation/canvas.json" 58 | } 59 | ] 60 | } 61 | ], 62 | "annotations": [ 63 | { 64 | "id": "http://localhost:4000/recipe/0139-geo-annotation/supplementingPage.json", 65 | "type": "AnnotationPage", 66 | "items": [ 67 | { 68 | "id": "http://localhost:4000/recipe/0139-geo-annotation/transcriptionAnno.json", 69 | "type": "Annotation", 70 | "motivation": "supplementing", 71 | "label": { 72 | "en": [ 73 | "Transcription text for this region of the canvas." 74 | ] 75 | }, 76 | "body": { 77 | "type": "TextualBody", 78 | "value": "Paris", 79 | "format": "text/plain", 80 | "language": "en", 81 | "purpose": "transcribing" 82 | }, 83 | "target": "http://localhost:4000/recipe/0139-geo-annotation/canvas.json#xywh=1300,3370,250,100" 84 | }, 85 | { 86 | "id": "http://localhost:4000/recipe/0139-geo-annotation/geoAnno.json", 87 | "type": "Annotation", 88 | "motivation": "geocode", 89 | "label": { 90 | "en": [ 91 | "The piece of the canvas containing the word 'Paris' geocoded to Paris, France." 92 | ] 93 | }, 94 | "body": { 95 | "id": "http://localhost:4000/recipe/0139-geo-annotation/geo.json", 96 | "type": "ExtensionType", 97 | "properties": { 98 | "label": { 99 | "en": [ 100 | "Paris, France" 101 | ] 102 | } 103 | }, 104 | "geometry": { 105 | "type": "Point", 106 | "coordinates": [ 107 | 48.86, 108 | 2.34 109 | ] 110 | } 111 | }, 112 | "target": "http://localhost:4000/recipe/0139-geo-annotation/canvas.json#xywh=1300,3370,250,100" 113 | } 114 | ] 115 | } 116 | ] 117 | } 118 | ] 119 | } 120 | -------------------------------------------------------------------------------- /fixtures/3/full_example.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "http://iiif.io/api/presentation/3/context.json", 3 | "id": "https://example.org/iiif/book1/manifest", 4 | "type": "Manifest", 5 | "label": { "en": [ "Book 1" ] }, 6 | "metadata": [ 7 | { 8 | "label": { "en": [ "Author" ] }, 9 | "value": { "none": [ "Anne Author" ] } 10 | }, 11 | { 12 | "label": { "en": [ "Published" ] }, 13 | "value": { 14 | "en": [ "Paris, circa 1400" ], 15 | "fr": [ "Paris, environ 1400" ] 16 | } 17 | }, 18 | { 19 | "label": { "en": [ "Notes" ] }, 20 | "value": { 21 | "en": [ 22 | "Text of note 1", 23 | "Text of note 2" 24 | ] 25 | } 26 | }, 27 | { 28 | "label": { "en": [ "Source" ] }, 29 | "value": { "none": [ "From: Some Collection" ] } 30 | } 31 | ], 32 | "summary": { "en": [ "Book 1, written be Anne Author, published in Paris around 1400." ] }, 33 | 34 | "thumbnail": [ 35 | { 36 | "id": "https://example.org/iiif/book1/page1/full/80,100/0/default.jpg", 37 | "type": "Image", 38 | "format": "image/jpeg", 39 | "service": [ 40 | { 41 | "id": "https://example.org/iiif/book1/page1", 42 | "type": "ImageService3", 43 | "profile": "level1" 44 | } 45 | ] 46 | } 47 | ], 48 | 49 | "viewingDirection": "right-to-left", 50 | "behavior": [ "paged" ], 51 | "navDate": "1856-01-01T00:00:00Z", 52 | 53 | "rights": "http://creativecommons.org/licenses/by/4.0/", 54 | "requiredStatement": { 55 | "label": { "en": [ "Attribution" ] }, 56 | "value": { "en": [ "Provided by Example Organization" ] } 57 | }, 58 | 59 | "provider": [ 60 | { 61 | "id": "https://example.org/about", 62 | "type": "Agent", 63 | "label": { "en": [ "Example Organization" ] }, 64 | "homepage": [ 65 | { 66 | "id": "https://example.org/", 67 | "type": "Text", 68 | "label": { "en": [ "Example Organization Homepage" ] }, 69 | "format": "text/html" 70 | } 71 | ], 72 | "logo": [ 73 | { 74 | "id": "https://example.org/service/inst1/full/max/0/default.png", 75 | "type": "Image", 76 | "format": "image/png", 77 | "service": [ 78 | { 79 | "id": "https://example.org/service/inst1", 80 | "type": "ImageService3", 81 | "profile": "level2" 82 | } 83 | ] 84 | } 85 | ], 86 | "seeAlso": [ 87 | { 88 | "id": "https://data.example.org/about/us.jsonld", 89 | "type": "Dataset", 90 | "format": "application/ld+json", 91 | "profile": "https://schema.org/" 92 | } 93 | ] 94 | } 95 | ], 96 | "homepage": [ 97 | { 98 | "id": "https://example.org/info/book1/", 99 | "type": "Text", 100 | "label": { "en": [ "Home page for Book 1" ] }, 101 | "format": "text/html" 102 | } 103 | ], 104 | "service": [ 105 | { 106 | "id": "https://example.org/service/example", 107 | "type": "ExampleExtensionService", 108 | "profile": "https://example.org/docs/example-service.html" 109 | } 110 | ], 111 | "seeAlso": [ 112 | { 113 | "id": "https://example.org/library/catalog/book1.xml", 114 | "type": "Dataset", 115 | "format": "text/xml", 116 | "profile": "https://example.org/profiles/bibliographic" 117 | } 118 | ], 119 | "rendering": [ 120 | { 121 | "id": "https://example.org/iiif/book1.pdf", 122 | "type": "Text", 123 | "label": { "en": [ "Download as PDF" ] }, 124 | "format": "application/pdf" 125 | } 126 | ], 127 | "partOf": [ 128 | { 129 | "id": "https://example.org/collections/books/", 130 | "type": "Collection" 131 | } 132 | ], 133 | "start": { 134 | "id": "https://example.org/iiif/book1/canvas/p2", 135 | "type": "Canvas" 136 | }, 137 | 138 | "services": [ 139 | { 140 | "@id": "https://example.org/iiif/auth/login", 141 | "@type": "AuthCookieService1", 142 | "profile": "http://iiif.io/api/auth/1/login", 143 | "label": "Login to Example Institution", 144 | "service": [ 145 | { 146 | "@id": "https://example.org/iiif/auth/token", 147 | "@type": "AuthTokenService1", 148 | "profile": "http://iiif.io/api/auth/1/token" 149 | } 150 | ] 151 | } 152 | ], 153 | 154 | "items": [ 155 | { 156 | "id": "https://example.org/iiif/book1/canvas/p1", 157 | "type": "Canvas", 158 | "label": { "none": [ "p. 1" ] }, 159 | "height": 1000, 160 | "width": 750, 161 | "items": [ 162 | { 163 | "id": "https://example.org/iiif/book1/page/p1/1", 164 | "type": "AnnotationPage", 165 | "items": [ 166 | { 167 | "id": "https://example.org/iiif/book1/annotation/p0001-image", 168 | "type": "Annotation", 169 | "motivation": "painting", 170 | "body": { 171 | "id": "https://example.org/iiif/book1/page1/full/max/0/default.jpg", 172 | "type": "Image", 173 | "format": "image/jpeg", 174 | "service": [ 175 | { 176 | "id": "https://example.org/iiif/book1/page1", 177 | "type": "ImageService3", 178 | "profile": "level2", 179 | "service": [ 180 | { 181 | "@id": "https://example.org/iiif/auth/login", 182 | "@type": "AuthCookieService1" 183 | } 184 | ] 185 | } 186 | ], 187 | "height": 2000, 188 | "width": 1500 189 | }, 190 | "target": "https://example.org/iiif/book1/canvas/p1" 191 | } 192 | ] 193 | } 194 | ], 195 | "annotations": [ 196 | { 197 | "id": "https://example.org/iiif/book1/comments/p1/1", 198 | "type": "AnnotationPage" 199 | } 200 | ] 201 | }, 202 | { 203 | "id": "https://example.org/iiif/book1/canvas/p2", 204 | "type": "Canvas", 205 | "label": { "none": [ "p. 2" ] }, 206 | "height": 1000, 207 | "width": 750, 208 | "items": [ 209 | { 210 | "id": "https://example.org/iiif/book1/page/p2/1", 211 | "type": "AnnotationPage", 212 | "items": [ 213 | { 214 | "id": "https://example.org/iiif/book1/annotation/p0002-image", 215 | "type": "Annotation", 216 | "motivation": "painting", 217 | "body": { 218 | "id": "https://example.org/iiif/book1/page2/full/max/0/default.jpg", 219 | "type": "Image", 220 | "format": "image/jpeg", 221 | "service": [ 222 | { 223 | "id": "https://example.org/iiif/book1/page2", 224 | "type": "ImageService3", 225 | "profile": "level2" 226 | } 227 | ], 228 | "height": 2000, 229 | "width": 1500 230 | }, 231 | "target": "https://example.org/iiif/book1/canvas/p2" 232 | } 233 | ] 234 | } 235 | ] 236 | } 237 | ], 238 | 239 | "structures": [ 240 | { 241 | "id": "https://example.org/iiif/book1/range/r0", 242 | "type": "Range", 243 | "label": { "en": [ "Table of Contents" ] }, 244 | "items": [ 245 | { 246 | "id": "https://example.org/iiif/book1/range/r1", 247 | "type": "Range", 248 | "label": { "en": [ "Introduction" ] }, 249 | "supplementary": { 250 | "id": "https://example.org/iiif/book1/annocoll/introTexts", 251 | "type": "AnnotationCollection" 252 | }, 253 | "items": [ 254 | { 255 | "id": "https://example.org/iiif/book1/canvas/p1", 256 | "type": "Canvas" 257 | }, 258 | { 259 | "type": "SpecificResource", 260 | "source": "https://example.org/iiif/book1/canvas/p2", 261 | "selector": { 262 | "type": "FragmentSelector", 263 | "value": "xywh=0,0,750,300" 264 | } 265 | } 266 | ] 267 | } 268 | ] 269 | } 270 | ], 271 | 272 | "annotations": [ 273 | { 274 | "id": "https://example.org/iiif/book1/page/manifest/1", 275 | "type": "AnnotationPage", 276 | "items": [ 277 | { 278 | "id": "https://example.org/iiif/book1/page/manifest/a1", 279 | "type": "Annotation", 280 | "motivation": "commenting", 281 | "body": { 282 | "type": "TextualBody", 283 | "language": "en", 284 | "value": "I love this manifest!" 285 | }, 286 | "target": "https://example.org/iiif/book1/manifest" 287 | } 288 | ] 289 | } 290 | ] 291 | } 292 | -------------------------------------------------------------------------------- /fixtures/3/multi_bodies.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "http://iiif.io/api/presentation/3/context.json", 3 | "id": "http://localhost:4000/recipe/0219-using-caption-file/manifest.json", 4 | "type": "Manifest", 5 | "label": { 6 | "en": [ 7 | "Lunchroom Manners" 8 | ] 9 | }, 10 | "items": [ 11 | { 12 | "id": "http://localhost:4000/recipe/0219-using-caption-file/canvas", 13 | "type": "Canvas", 14 | "height": 360, 15 | "width": 480, 16 | "duration": 572.034, 17 | "items": [ 18 | { 19 | "id": "http://localhost:4000/recipe/0219-using-caption-file/canvas/page", 20 | "type": "AnnotationPage", 21 | "items": [ 22 | { 23 | "id": "http://localhost:4000/recipe/0219-using-caption-file/canvas/page/annotation", 24 | "type": "Annotation", 25 | "motivation": "painting", 26 | "body": [ 27 | { 28 | "id": "https://fixtures.iiif.io/video/indiana/lunchroom_manners/high/lunchroom_manners_1024kb.mp4", 29 | "type": "Video", 30 | "height": 360, 31 | "width": 480, 32 | "duration": 572.034, 33 | "format": "video/mp4" 34 | }, 35 | { 36 | "id": "https://fixtures.iiif.io/video/indiana/lunchroom_manners/lunchroom_manners.vtt", 37 | "type": "Text", 38 | "format": "text/vtt", 39 | "language": "en" 40 | } 41 | ], 42 | "target": "http://localhost:4000/recipe/0219-using-caption-file/canvas" 43 | } 44 | ] 45 | } 46 | ] 47 | } 48 | ] 49 | } -------------------------------------------------------------------------------- /fixtures/3/navPlace.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context" : [ 3 | "http://iiif.io/api/extension/navplace/context.json", 4 | "http://iiif.io/api/presentation/3/context.json" 5 | ], 6 | "id" : "https://iiif.io/api/cookbook/recipe/0154-geo-extension/manifest.json", 7 | "type" : "Manifest", 8 | "label" : { 9 | "it" : [ "Bronzo Laocoonte e i suoi figli" ] 10 | }, 11 | "navPlace" : { 12 | "type" : "FeatureCollection", 13 | "features" : [ { 14 | "type" : "Feature", 15 | "properties" : { 16 | "label" : { 17 | "en" : [ "The Laocoön Bronze" ], 18 | "it" : [ "Bronzo Laocoonte e i suoi figli" ] 19 | } 20 | }, 21 | "geometry" : { 22 | "type" : "Point", 23 | "coordinates" : [ -118.4745559, 34.0776376 ] 24 | } 25 | } ] 26 | }, 27 | "items" : [ { 28 | "id": "https://iiif.io/api/cookbook/recipe/0154-geo-extension/canvas/1", 29 | "type" : "Canvas", 30 | "height" : 3000, 31 | "width" : 2315, 32 | "label" : { 33 | "en" : [ "Front of Bronze" ] 34 | }, 35 | "items" : [ { 36 | "id" : "https://iiif.io/api/cookbook/recipe/0154-geo-extension/anno-page/1", 37 | "type" : "AnnotationPage", 38 | "items" : [ { 39 | "id" : "https://iiif.io/api/cookbook/recipe/0154-geo-extension/anno/1", 40 | "type" : "Annotation", 41 | "motivation" : "painting", 42 | "body" : { 43 | "id" : "https://iiif.io/api/image/3.0/example/reference/28473c77da3deebe4375c3a50572d9d3-laocoon/full/max/0/default.jpg", 44 | "type" : "Image", 45 | "format" : "image/jpg", 46 | "height" : 3000, 47 | "width" : 2315, 48 | "service" : [ { 49 | "id" : "https://iiif.io/api/image/3.0/example/reference/28473c77da3deebe4375c3a50572d9d3-laocoon", 50 | "profile" : "level1", 51 | "type" : "ImageService3" 52 | } ] 53 | }, 54 | "target" : "https://iiif.io/api/cookbook/recipe/0154-geo-extension/canvas/1" 55 | } ] 56 | } ] 57 | } ] 58 | } 59 | -------------------------------------------------------------------------------- /fixtures/3/non_cc_license.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "http://iiif.io/api/presentation/3/context.json", 3 | "id": "http://localhost:4000/recipe/0008-rights/manifest.json", 4 | "type": "Manifest", 5 | "label": { 6 | "en": [ 7 | "Picture of Göttingen taken during the 2019 IIIF Conference" 8 | ] 9 | }, 10 | "summary": { 11 | "en": [ 12 | "

Picture taken by the IIIF Technical Coordinator

" 13 | ] 14 | }, 15 | "rights": "https://en.wikipedia.org/wiki/License", 16 | "requiredStatement": { 17 | "label": { 18 | "en": [ 19 | "Attribution" 20 | ] 21 | }, 22 | "value": { 23 | "en": [ 24 | "Glen Robson, IIIF Technical Coordinator. CC BY-SA 3.0 " 25 | ] 26 | } 27 | }, 28 | "items": [ 29 | { 30 | "id": "http://localhost:4000/recipe/0008-rights/canvas/p1", 31 | "type": "Canvas", 32 | "height": 3024, 33 | "width": 4032, 34 | "items": [ 35 | { 36 | "id": "http://localhost:4000/recipe/0008-rights/page/p1/1", 37 | "type": "AnnotationPage", 38 | "items": [ 39 | { 40 | "id": "http://localhost:4000/recipe/0008-rights/annotation/p0001-image", 41 | "type": "Annotation", 42 | "motivation": "painting", 43 | "body": { 44 | "id": "https://iiif.io/api/image/3.0/example/reference/918ecd18c2592080851777620de9bcb5-gottingen/full/max/0/default.jpg", 45 | "type": "Image", 46 | "format": "image/jpeg", 47 | "height": 3024, 48 | "width": 4032, 49 | "service": [ 50 | { 51 | "id": "https://iiif.io/api/image/3.0/example/reference/918ecd18c2592080851777620de9bcb5-gottingen", 52 | "profile": "level1", 53 | "type": "ImageService3" 54 | } 55 | ] 56 | }, 57 | "target": "http://localhost:4000/recipe/0008-rights/canvas/p1" 58 | } 59 | ] 60 | } 61 | ] 62 | } 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /fixtures/3/old_cc_license.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "http://iiif.io/api/presentation/3/context.json", 3 | "id": "http://localhost:4000/recipe/0008-rights/manifest.json", 4 | "type": "Manifest", 5 | "label": { 6 | "en": [ 7 | "Picture of Göttingen taken during the 2019 IIIF Conference" 8 | ] 9 | }, 10 | "summary": { 11 | "en": [ 12 | "

Picture taken by the IIIF Technical Coordinator

" 13 | ] 14 | }, 15 | "rights": "http://creativecommons.org/licenses/by-sa/3.0/", 16 | "requiredStatement": { 17 | "label": { 18 | "en": [ 19 | "Attribution" 20 | ] 21 | }, 22 | "value": { 23 | "en": [ 24 | "Glen Robson, IIIF Technical Coordinator. CC BY-SA 3.0 " 25 | ] 26 | } 27 | }, 28 | "items": [ 29 | { 30 | "id": "http://localhost:4000/recipe/0008-rights/canvas/p1", 31 | "type": "Canvas", 32 | "height": 3024, 33 | "width": 4032, 34 | "items": [ 35 | { 36 | "id": "http://localhost:4000/recipe/0008-rights/page/p1/1", 37 | "type": "AnnotationPage", 38 | "items": [ 39 | { 40 | "id": "http://localhost:4000/recipe/0008-rights/annotation/p0001-image", 41 | "type": "Annotation", 42 | "motivation": "painting", 43 | "body": { 44 | "id": "https://iiif.io/api/image/3.0/example/reference/918ecd18c2592080851777620de9bcb5-gottingen/full/max/0/default.jpg", 45 | "type": "Image", 46 | "format": "image/jpeg", 47 | "height": 3024, 48 | "width": 4032, 49 | "service": [ 50 | { 51 | "id": "https://iiif.io/api/image/3.0/example/reference/918ecd18c2592080851777620de9bcb5-gottingen", 52 | "profile": "level1", 53 | "type": "ImageService3" 54 | } 55 | ] 56 | }, 57 | "target": "http://localhost:4000/recipe/0008-rights/canvas/p1" 58 | } 59 | ] 60 | } 61 | ] 62 | } 63 | ] 64 | } -------------------------------------------------------------------------------- /fixtures/3/old_format_label.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": [ 3 | "http://www.w3.org/ns/anno.jsonld", 4 | "http://iiif.io/api/presentation/3/context.json" 5 | ], 6 | "id": "https://example.com/manifest", 7 | "label": "Old table label which doesn't have a language", 8 | "type": "Manifest", 9 | "sequences": [ 10 | { 11 | "id": "https://example.com/sequence", 12 | "label": "Current order", 13 | "type": "Sequence", 14 | "viewingDirection": "left-to-right", 15 | "canvases": [ 16 | { 17 | "type": "Canvas", 18 | "id": "https://example.com/canvas1", 19 | "label": "Image 1", 20 | "height": 3820, 21 | "width": 5426, 22 | "content": [ 23 | { 24 | "type": "AnnotationPage", 25 | "id": "https://example.com/anno1", 26 | "items": [ 27 | { 28 | "id": "https://example.com/image1", 29 | "type": "Annotation", 30 | "motivation": "painting", 31 | "target": "https://example.com/canvas1", 32 | "body": { 33 | "type": "Image", 34 | "id": "https://example.com/image/full.jpg", 35 | "format": "image/jpeg", 36 | "height": 3820, 37 | "width": 5426, 38 | "service": { 39 | "@context": "http://iiif.io/api/image/2/context.json", 40 | "id": "https://example.com/image1/api/1", 41 | "profile": "http://iiif.io/api/image/2/level2.json" 42 | } 43 | } 44 | } 45 | ] 46 | } 47 | ] 48 | } 49 | ] 50 | } 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /fixtures/3/placeholderCanvas.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "http://iiif.io/api/presentation/3/context.json", 3 | "id": "https://iiif.io/api/cookbook/recipe/0013-placeholderCanvas/manifest.json", 4 | "type": "Manifest", 5 | "label": { 6 | "en": [ 7 | "Video recording of Donizetti's _The Elixer of Love_" 8 | ] 9 | }, 10 | "items": [ 11 | { 12 | "id": "https://iiif.io/api/cookbook/recipe/0013-placeholderCanvas/canvas/donizetti", 13 | "type": "Canvas", 14 | "duration": 7278.466, 15 | "width": 640, 16 | "height": 360, 17 | "placeholderCanvas": { 18 | "id": "https://iiif.io/api/cookbook/recipe/0013-placeholderCanvas/canvas/donizetti/placeholder", 19 | "type": "Canvas", 20 | "width": 640, 21 | "height": 360, 22 | "items": [ 23 | { 24 | "id": "https://iiif.io/api/cookbook/recipe/0013-placeholderCanvas/canvas/donizetti/placeholder/1", 25 | "type": "AnnotationPage", 26 | "items": [ 27 | { 28 | "id": "https://iiif.io/api/cookbook/recipe/0013-placeholderCanvas/canvas/donizetti/placeholder/1-image", 29 | "type": "Annotation", 30 | "motivation": "painting", 31 | "body": { 32 | "id": "https://fixtures.iiif.io/video/indiana/donizetti-elixir/act1-thumbnail.png", 33 | "type": "Image", 34 | "format": "image/png", 35 | "width": 640, 36 | "height": 360 37 | }, 38 | "target": "https://iiif.io/api/cookbook/recipe/0013-placeholderCanvas/canvas/donizetti/placeholder" 39 | } 40 | ] 41 | } 42 | ] 43 | }, 44 | "items": [ 45 | { 46 | "id": "https://iiif.io/api/cookbook/recipe/0013-placeholderCanvas/donizetti/1", 47 | "type": "AnnotationPage", 48 | "items": [ 49 | { 50 | "id": "https://iiif.io/api/cookbook/recipe/0013-placeholderCanvas/donizetti/1-video", 51 | "type": "Annotation", 52 | "motivation": "painting", 53 | "body": { 54 | "id": "https://fixtures.iiif.io/video/indiana/donizetti-elixir/vae0637_accessH264_low.mp4", 55 | "type": "Video", 56 | "duration": 7278.466, 57 | "width": 640, 58 | "height": 360, 59 | "format": "video/mp4" 60 | }, 61 | "target": "https://iiif.io/api/cookbook/recipe/0013-placeholderCanvas/canvas/donizetti" 62 | } 63 | ] 64 | } 65 | ] 66 | } 67 | ] 68 | } -------------------------------------------------------------------------------- /fixtures/3/point_selector.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "http://iiif.io/api/presentation/3/context.json", 3 | "id": "http://localhost:4000/recipe/0135-annotating-point-in-canvas/manifest.json", 4 | "type": "Manifest", 5 | "label": { 6 | "en": [ 7 | "Using a point selector for annotating a location on a map." 8 | ] 9 | }, 10 | "summary": { 11 | "en": [ 12 | "A map containing an point with an annotation of the location." 13 | ] 14 | }, 15 | "items": [ 16 | { 17 | "id": "http://localhost:4000/recipe/0135-annotating-point-in-canvas/canvas.json", 18 | "type": "Canvas", 19 | "label": { 20 | "en": [ 21 | "Chesapeake and Ohio Canal Pamphlet" 22 | ] 23 | }, 24 | "height": 5212, 25 | "width": 7072, 26 | "items": [ 27 | { 28 | "id": "http://localhost:4000/recipe/0135-annotating-point-in-canvas/contentPage.json", 29 | "type": "AnnotationPage", 30 | "items": [ 31 | { 32 | "id": "http://localhost:4000/recipe/0135-annotating-point-in-canvas/content.json", 33 | "type": "Annotation", 34 | "motivation": "painting", 35 | "body": { 36 | "id": "https://iiif.io/api/image/3.0/example/reference/43153e2ec7531f14dd1c9b2fc401678a-88695674/full/max/0/default.jpg", 37 | "type": "Image", 38 | "format": "image/jpeg", 39 | "height": 5212, 40 | "width": 7072, 41 | "service": [ 42 | { 43 | "id": "https://iiif.io/api/image/3.0/example/reference/43153e2ec7531f14dd1c9b2fc401678a-88695674", 44 | "type": "ImageService3", 45 | "profile": "level1" 46 | } 47 | ] 48 | }, 49 | "target": "http://localhost:4000/recipe/0135-annotating-point-in-canvas/canvas.json" 50 | } 51 | ] 52 | } 53 | ], 54 | "annotations": [ 55 | { 56 | "id": "http://localhost:4000/recipe/0135-annotating-point-in-canvas/page/p2/1", 57 | "type": "AnnotationPage", 58 | "items": [ 59 | { 60 | "id": "http://localhost:4000/recipe/0135-annotating-point-in-canvas/annotation/p0002-tag", 61 | "type": "Annotation", 62 | "label": { 63 | "en": [ 64 | "Annotation containing the name of the place annotated using the PointSelector." 65 | ] 66 | }, 67 | "motivation": "tagging", 68 | "body": { 69 | "type": "TextualBody", 70 | "value": "Town Creek Aqueduct", 71 | "language": "en", 72 | "format": "text/plain" 73 | }, 74 | "target": { 75 | "type": "SpecificResource", 76 | "source": "http://localhost:4000/recipe/0135-annotating-point-in-canvas/canvas.json", 77 | "selector": { 78 | "type": "PointSelector", 79 | "x": 3385, 80 | "y": 1464 81 | } 82 | } 83 | } 84 | ] 85 | } 86 | ] 87 | } 88 | ] 89 | } -------------------------------------------------------------------------------- /fixtures/3/publicdomain.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "http://iiif.io/api/presentation/3/context.json", 3 | "id": "http://localhost:4000/recipe/0008-rights/manifest.json", 4 | "type": "Manifest", 5 | "label": { 6 | "en": [ 7 | "Testing public domain" 8 | ] 9 | }, 10 | "rights": "http://creativecommons.org/publicdomain/zero/1.0/", 11 | "items": [ 12 | { 13 | "id": "http://localhost:4000/recipe/0008-rights/canvas/p1", 14 | "type": "Canvas", 15 | "height": 3024, 16 | "width": 4032, 17 | "items": [ 18 | { 19 | "id": "http://localhost:4000/recipe/0008-rights/page/p1/1", 20 | "type": "AnnotationPage", 21 | "items": [ 22 | { 23 | "id": "http://localhost:4000/recipe/0008-rights/annotation/p0001-image", 24 | "type": "Annotation", 25 | "motivation": "painting", 26 | "body": { 27 | "id": "https://iiif.io/api/image/3.0/example/reference/918ecd18c2592080851777620de9bcb5-gottingen/full/max/0/default.jpg", 28 | "type": "Image", 29 | "format": "image/jpeg", 30 | "height": 3024, 31 | "width": 4032, 32 | "service": [ 33 | { 34 | "id": "https://iiif.io/api/image/3.0/example/reference/918ecd18c2592080851777620de9bcb5-gottingen", 35 | "profile": "level1", 36 | "type": "ImageService3" 37 | } 38 | ] 39 | }, 40 | "target": "http://localhost:4000/recipe/0008-rights/canvas/p1" 41 | } 42 | ] 43 | } 44 | ] 45 | } 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /fixtures/3/range_range.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "http://iiif.io/api/presentation/3/context.json", 3 | "id": "https://example.org/iiif/book1/manifest", 4 | "type": "Manifest", 5 | "label": { "en": [ "Range containng ranges" ] }, 6 | "items": [ 7 | { 8 | "id": "https://example.org/iiif/book1/canvas/p1", 9 | "type": "Canvas", 10 | "label": { "none": [ "p. 1" ] }, 11 | "height": 1000, 12 | "width": 750, 13 | "items": [ 14 | { 15 | "id": "https://example.org/iiif/book1/page/p1/1", 16 | "type": "AnnotationPage", 17 | "items": [ 18 | { 19 | "id": "https://example.org/iiif/book1/annotation/p0001-image", 20 | "type": "Annotation", 21 | "motivation": "painting", 22 | "body": { 23 | "id": "https://example.org/iiif/book1/page1/full/max/0/default.jpg", 24 | "type": "Image", 25 | "format": "image/jpeg", 26 | "service": [ 27 | { 28 | "id": "https://example.org/iiif/book1/page1", 29 | "type": "ImageService3", 30 | "profile": "level2" 31 | } 32 | ], 33 | "height": 2000, 34 | "width": 1500 35 | }, 36 | "target": "https://example.org/iiif/book1/canvas/p1" 37 | } 38 | ] 39 | } 40 | ] 41 | }, 42 | { 43 | "id": "https://example.org/iiif/book1/canvas/p2", 44 | "type": "Canvas", 45 | "label": { "none": [ "p. 2" ] }, 46 | "height": 1000, 47 | "width": 750, 48 | "items": [ 49 | { 50 | "id": "https://example.org/iiif/book1/page/p2/1", 51 | "type": "AnnotationPage", 52 | "items": [ 53 | { 54 | "id": "https://example.org/iiif/book1/annotation/p0002-image", 55 | "type": "Annotation", 56 | "motivation": "painting", 57 | "body": { 58 | "id": "https://example.org/iiif/book1/page2/full/max/0/default.jpg", 59 | "type": "Image", 60 | "format": "image/jpeg", 61 | "service": [ 62 | { 63 | "id": "https://example.org/iiif/book1/page2", 64 | "type": "ImageService3", 65 | "profile": "level2" 66 | } 67 | ], 68 | "height": 2000, 69 | "width": 1500 70 | }, 71 | "target": "https://example.org/iiif/book1/canvas/p2" 72 | } 73 | ] 74 | } 75 | ] 76 | } 77 | ], 78 | 79 | "structures": [ 80 | { 81 | "id": "https://example.org/iiif/book1/range/r0", 82 | "type": "Range", 83 | "label": { "en": [ "Table of Contents" ] }, 84 | "items": [ 85 | { 86 | "id": "https://example.org/iiif/book1/range/r1", 87 | "type": "Range", 88 | "label": { "en": [ "toc2" ] }, 89 | "items": [ 90 | { 91 | "id": "https://example.org/iiif/book1/canvas/p1", 92 | "type": "Canvas" 93 | }, 94 | { 95 | "id": "https://example.org/iiif/book1/range/r1", 96 | "type": "Range", 97 | "label": { "en": [ "toc2" ] }, 98 | "items":[{ 99 | "id": "https://example.org/iiif/book1/canvas/p2", 100 | "type": "Canvas" 101 | }] 102 | } 103 | ] 104 | } 105 | ] 106 | } 107 | ] 108 | } 109 | -------------------------------------------------------------------------------- /fixtures/3/rights_lang_issues.json: -------------------------------------------------------------------------------- 1 | {"@context":["http://www.w3.org/ns/anno.jsonld","http://iiif.io/api/presentation/3/context.json"],"id":"https://www.rpm-api.io/records/5e78f06706f6190017c039f9?_format=iiif","type":"Manifest","items":[{"id":"https://www.rpm-api.io/records/5e78f06706f6190017c039f9/canvas/0","type":"Canvas","items":[{"id":"https://www.rpm-api.io/records/5e78f06706f6190017c039f9/canvas/0/annotationpage/0","type":"AnnotationPage","items":[{"id":"https://www.rpm-api.io/records/5e78f06706f6190017c039f9/canvas/0/annotation/0","type":"Annotation","motivation":"painting","body":{"id":"https://iiif.rpm-api.io/iiif/2/d300bde0b6954976b3a9534160768aa6/full/max/0/default.jpg","type":"Image","format":"image/jpeg","label":{"@none":["Map of Brighthelmston"]},"service":[{"id":"https://iiif.rpm-api.io/iiif/2/d300bde0b6954976b3a9534160768aa6","type":"ImageService2","profile":"http://iiif.io/api/image/2/level2.json"}]},"target":"https://www.rpm-api.io/records/5e78f06706f6190017c039f9/canvas/0"}]}],"label":{"@none":["Map of Brighthelmston"]},"thumbnail":[{"id":"https://iiif.rpm-api.io/iiif/2/d300bde0b6954976b3a9534160768aa6/full/90,/0/default.jpg","type":"Image"}]}],"label":{"@none":["Map of Brighthelmston"]},"metadata":[{"label":{"@none":["Title"]},"value":{"@none":["Map of Brighthelmston"]}},{"label":{"@none":["Department"]},"value":{"@none":["Fine Art"]}},{"label":{"@none":["Category"]},"value":{"@none":["Coloured Lithograph"]}},{"label":{"@none":["Creator"]},"value":{"@none":["Yeakell; Gardner; Arthur Wallis"]}},{"label":{"@none":["Place Created"]},"value":{"@none":["Brighton"]}},{"label":{"@none":["Date Created"]},"value":{"@none":["1779"]}},{"label":{"@none":["Materials"]},"value":{"@none":["Paper"]}},{"label":{"@none":["Description"]},"value":{"@none":[" \tThis is a coloured version which has been mounted. It shows the Old Town of Brighton and is annotated and shows the gardens at the back of houses and the open spaces and market gardens in some detail. At the top and bottom of the map are two engraved illustrations one of which is of the town from the sea. The references mention all the main buildigs. "]}},{"label":{"@none":["Licence"]},"value":{"@none":["https://creativecommons.org/publicdomain/zero/1.0/"]}},{"label":{"@none":["Accession Id"]},"value":{"@none":["FATMP000096"]}}],"rights":"https://creativecommons.org/publicdomain/zero/1.0/"} -------------------------------------------------------------------------------- /fixtures/3/rightsstatement_license.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "http://iiif.io/api/presentation/3/context.json", 3 | "id": "http://localhost:4000/recipe/0008-rights/manifest.json", 4 | "type": "Manifest", 5 | "label": { 6 | "en": [ 7 | "Picture of Göttingen taken during the 2019 IIIF Conference" 8 | ] 9 | }, 10 | "summary": { 11 | "en": [ 12 | "

Picture taken by the IIIF Technical Coordinator

" 13 | ] 14 | }, 15 | "rights": "http://rightsstatements.org/vocab/NoC-NC/1.0/", 16 | "requiredStatement": { 17 | "label": { 18 | "en": [ 19 | "Attribution" 20 | ] 21 | }, 22 | "value": { 23 | "en": [ 24 | "Glen Robson, IIIF Technical Coordinator. CC BY-SA 3.0 " 25 | ] 26 | } 27 | }, 28 | "items": [ 29 | { 30 | "id": "http://localhost:4000/recipe/0008-rights/canvas/p1", 31 | "type": "Canvas", 32 | "height": 3024, 33 | "width": 4032, 34 | "items": [ 35 | { 36 | "id": "http://localhost:4000/recipe/0008-rights/page/p1/1", 37 | "type": "AnnotationPage", 38 | "items": [ 39 | { 40 | "id": "http://localhost:4000/recipe/0008-rights/annotation/p0001-image", 41 | "type": "Annotation", 42 | "motivation": "painting", 43 | "body": { 44 | "id": "https://iiif.io/api/image/3.0/example/reference/918ecd18c2592080851777620de9bcb5-gottingen/full/max/0/default.jpg", 45 | "type": "Image", 46 | "format": "image/jpeg", 47 | "height": 3024, 48 | "width": 4032, 49 | "service": [ 50 | { 51 | "id": "https://iiif.io/api/image/3.0/example/reference/918ecd18c2592080851777620de9bcb5-gottingen", 52 | "profile": "level1", 53 | "type": "ImageService3" 54 | } 55 | ] 56 | }, 57 | "target": "http://localhost:4000/recipe/0008-rights/canvas/p1" 58 | } 59 | ] 60 | } 61 | ] 62 | } 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /fixtures/3/simple_video.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": [ 3 | "http://www.w3.org/ns/anno.jsonld", 4 | "http://iiif.io/api/presentation/3/context.json" 5 | ], 6 | "type": "Manifest", 7 | "id": "https://iiif.gdmrdigital.com/nlw/LLOYD_GEORGE_FARMING-mp4.json", 8 | "label": { 9 | "en": [ 10 | "Lloyd George: farming, officiating and breakfasting" 11 | ] 12 | }, 13 | "metadata": [ 14 | { 15 | "label": { 16 | "en": [ 17 | "Title" 18 | ] 19 | }, 20 | "value": { 21 | "en": [ 22 | "Lloyd George: farming, officiating and breakfasting" 23 | ] 24 | } 25 | } 26 | ], 27 | "items": [ 28 | { 29 | "type": "Canvas", 30 | "id": "https://iiif.gdmrdigital.com/nlw/LLOYD_GEORGE_FARMING/canvas/1", 31 | "thumbnail": [ 32 | { 33 | "id": "https://datasyllwr.llgc.org.uk/video/dlg/LLOYD_GEORGE_FARMING.jpg", 34 | "type": "Image" 35 | } 36 | ], 37 | "items": [ 38 | { 39 | "type": "AnnotationPage", 40 | "id": "https://iiif.gdmrdigital.com/nlw/LLOYD_GEORGE_FARMING/AnnotationPage/1", 41 | "items": [ 42 | { 43 | "id": "https://iiif.gdmrdigital.com/nlw/LLOYD_GEORGE_FARMING/Annotation/1", 44 | "type": "Annotation", 45 | "motivation": "painting", 46 | "target": "https://iiif.gdmrdigital.com/nlw/LLOYD_GEORGE_FARMING/canvas/1", 47 | "body": { 48 | "id": "https://datasyllwr.llgc.org.uk/video/dlg/LLOYD_GEORGE_FARMING.mp4", 49 | "type": "Video", 50 | "height": 360, 51 | "width": 480, 52 | "duration": 765, 53 | "format": "video/mp4" 54 | } 55 | } 56 | ] 57 | } 58 | ], 59 | "width": 480, 60 | "height": 360, 61 | "duration": 765 62 | } 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /fixtures/3/version2image.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "http://iiif.io/api/presentation/3/context.json", 3 | "id": "https://example.org/iiif/book1/manifest", 4 | "type": "Manifest", 5 | "label": { "en": [ "Book 1" ] }, 6 | "items": [ 7 | { 8 | "id": "https://example.org/iiif/book1/canvas/p1", 9 | "type": "Canvas", 10 | "label": { "none": [ "p. 1" ] }, 11 | "height": 1000, 12 | "width": 750, 13 | "items": [ 14 | { 15 | "id": "https://example.org/iiif/book1/page/p1/1", 16 | "type": "AnnotationPage", 17 | "items": [ 18 | { 19 | "id": "https://example.org/iiif/book1/annotation/p0001-image", 20 | "type": "Annotation", 21 | "motivation": "painting", 22 | "body": { 23 | "id": "https://example.org/iiif/book1/page1/full/full/0/default.jpg", 24 | "type": "Image", 25 | "format": "image/jpeg", 26 | "service": [ 27 | { 28 | "@id": "https://example.org/iiif2/image1/identifier", 29 | "@type": "ImageService2", 30 | "profile": "http://iiif.io/api/image/2/level2.json" 31 | } 32 | ], 33 | "height": 2000, 34 | "width": 1500 35 | }, 36 | "target": "https://example.org/iiif/book1/canvas/p1" 37 | } 38 | ] 39 | } 40 | ], 41 | "annotations": [ 42 | { 43 | "id": "https://example.org/iiif/book1/comments/p1/1", 44 | "type": "AnnotationPage" 45 | } 46 | ] 47 | } 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /fixtures/README.md: -------------------------------------------------------------------------------- 1 | # Test Fixtures 2 | 3 | These fixtures are taken from the the Presentation API specifications under or . 4 | -------------------------------------------------------------------------------- /iiif-presentation-validator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | """IIIF Presentation Validation Service.""" 4 | 5 | import argparse 6 | import codecs 7 | import json 8 | import os 9 | from gzip import GzipFile 10 | from io import BytesIO 11 | from jsonschema.exceptions import ValidationError, SchemaError 12 | import traceback 13 | 14 | try: 15 | # python3 16 | from urllib.request import urlopen, HTTPError, Request 17 | from urllib.parse import urlparse 18 | except ImportError: 19 | # fall back to python2 20 | from urllib2 import urlopen, HTTPError, Request 21 | from urlparse import urlparse 22 | 23 | from bottle import Bottle, request, response, run 24 | from schema import schemavalidator 25 | 26 | egg_cache = "/path/to/web/egg_cache" 27 | os.environ['PYTHON_EGG_CACHE'] = egg_cache 28 | 29 | from iiif_prezi.loader import ManifestReader 30 | from pyld import jsonld 31 | jsonld.set_document_loader(jsonld.requests_document_loader(timeout=60)) 32 | 33 | IIIF_HEADER = "application/ld+json;profile=http://iiif.io/api/presentation/{iiif_version}/context.json" 34 | 35 | class Validator(object): 36 | """Validator class that runs with Bottle.""" 37 | 38 | def __init__(self): 39 | """Initialize Validator with default_version.""" 40 | self.default_version = "2.1" 41 | 42 | def fetch(self, url, accept_header): 43 | """Fetch manifest from url.""" 44 | req = Request(url) 45 | req.add_header('User-Agent', 'IIIF Validation Service') 46 | req.add_header('Accept-Encoding', 'gzip') 47 | 48 | if accept_header: 49 | req.add_header('Accept', accept_header) 50 | 51 | try: 52 | wh = urlopen(req) 53 | except HTTPError as wh: 54 | raise wh 55 | data = wh.read() 56 | wh.close() 57 | 58 | if wh.headers.get('Content-Encoding') == 'gzip': 59 | with GzipFile(fileobj=BytesIO(data)) as f: 60 | data = f.read() 61 | 62 | try: 63 | data = data.decode('utf-8') 64 | except: 65 | raise 66 | return(data, wh) 67 | 68 | def check_manifest(self, data, version, url=None, warnings=[]): 69 | """Check manifest data at version, return JSON.""" 70 | infojson = {} 71 | # Check if 3.0 if so run through schema rather than this version... 72 | if version == '3.0': 73 | try: 74 | infojson = schemavalidator.validate(data, version, url) 75 | for error in infojson['errorList']: 76 | error.pop('error', None) 77 | 78 | mf = json.loads(data) 79 | if url and 'id' in mf and mf['id'] != url: 80 | raise ValidationError("The manifest id ({}) should be the same as the URL it is published at ({}).".format(mf["id"], url)) 81 | except ValidationError as e: 82 | if infojson: 83 | infojson['errorList'].append({ 84 | 'title': 'Resolve Error', 85 | 'detail': str(e), 86 | 'description': '', 87 | 'path': '/id', 88 | 'context': '{ \'id\': \'...\'}' 89 | }) 90 | else: 91 | infojson = { 92 | 'okay': 0, 93 | 'error': str(e), 94 | 'url': url, 95 | 'warnings': [] 96 | } 97 | except Exception as e: 98 | traceback.print_exc() 99 | infojson = { 100 | 'okay': 0, 101 | 'error': 'Presentation Validator bug: "{}". Please create a Validator Issue, including a link to the manifest.'.format(e), 102 | 'url': url, 103 | 'warnings': [] 104 | } 105 | 106 | else: 107 | reader = ManifestReader(data, version=version) 108 | err = None 109 | try: 110 | mf = reader.read() 111 | mf.toJSON() 112 | if url and mf.id != url: 113 | raise ValidationError("Manifest @id ({}) is different to the location where it was retrieved ({})".format(mf.id, url)) 114 | # Passed! 115 | okay = 1 116 | except KeyError as e: 117 | print ('Failed validation due to:') 118 | traceback.print_exc() 119 | err = 'Failed due to KeyError {}, check trace for details'.format(e) 120 | okay = 0 121 | except Exception as e: 122 | # Failed 123 | print ('Failed validation due to:') 124 | traceback.print_exc() 125 | err = e 126 | okay = 0 127 | 128 | warnings.extend(reader.get_warnings()) 129 | infojson = { 130 | 'okay': okay, 131 | 'warnings': warnings, 132 | 'error': str(err), 133 | 'url': url 134 | } 135 | return self.return_json(infojson) 136 | 137 | def return_json(self, js): 138 | """Set header and return JSON response.""" 139 | response.content_type = "application/json" 140 | return json.dumps(js) 141 | 142 | def do_POST_test(self): 143 | """Implement POST request to test posted data at default version.""" 144 | data = request.json 145 | if not data: 146 | b = request._get_body_string() 147 | try: 148 | b = b.decode('utf-8') 149 | except: 150 | pass 151 | data = json.loads(b) 152 | return self.check_manifest(data, self.default_version) 153 | 154 | def do_GET_test(self): 155 | """Implement GET request to test url at version.""" 156 | url = request.query.get('url', '') 157 | version = request.query.get('version', self.default_version) 158 | accept = request.query.get('accept') 159 | accept_header = None 160 | url = url.strip() 161 | parsed_url = urlparse(url) 162 | 163 | if accept and accept == 'true': 164 | if version in ("2.0", "2.1"): 165 | accept_header = IIIF_HEADER.format(iiif_version=2) 166 | elif version in ("3.0",): 167 | accept_header = IIIF_HEADER.format(iiif_version=3) 168 | else: 169 | accept_header = "application/json" 170 | 171 | if (parsed_url.scheme != 'http' and parsed_url.scheme != 'https'): 172 | return self.return_json({'okay': 0, 'error': 'URLs must use HTTP or HTTPS', 'url': url}) 173 | 174 | try: 175 | (data, webhandle) = self.fetch(url, accept_header) 176 | except Exception as error: 177 | return self.return_json({'okay': 0, 'error': 'Cannot fetch url. Got "{}"'.format(error), 'url': url}) 178 | 179 | # First check HTTP level 180 | ct = webhandle.headers.get('content-type', '') 181 | cors = webhandle.headers.get('access-control-allow-origin', '') 182 | 183 | warnings = [] 184 | if not ct.startswith('application/json') and not ct.startswith('application/ld+json'): 185 | # not json 186 | warnings.append("URL does not have correct content-type header: got \"%s\", expected JSON" % ct) 187 | if cors != "*": 188 | warnings.append("URL does not have correct access-control-allow-origin header:" 189 | " got \"%s\", expected *" % cors) 190 | 191 | content_encoding = webhandle.headers.get('Content-Encoding', '') 192 | if content_encoding != 'gzip': 193 | warnings.append('The remote server did not use the requested gzip' 194 | ' transfer compression, which will slow access.' 195 | ' (Content-Encoding: %s)' % content_encoding) 196 | elif 'Accept-Encoding' not in webhandle.headers.get('Vary', ''): 197 | warnings.append('gzip transfer compression is enabled but the Vary' 198 | ' header does not include Accept-Encoding, which' 199 | ' can cause compatibility issues') 200 | 201 | return self.check_manifest(data, version, url, warnings) 202 | 203 | def index_route(self): 204 | """Read and return index page.""" 205 | with codecs.open(os.path.join(os.path.dirname(__file__), 'index.html'), 'r', 'utf-8') as fh: 206 | data = fh.read() 207 | return data 208 | 209 | def dispatch_views(self): 210 | """Set up path mappings.""" 211 | self.app.route("/", "GET", self.index_route) 212 | self.app.route("/validate", "OPTIONS", self.empty_response) 213 | self.app.route("/validate", "GET", self.do_GET_test) 214 | self.app.route("/validate", "POST", self.do_POST_test) 215 | 216 | def after_request(self): 217 | """Used with after_request hook to set response headers.""" 218 | methods = 'GET,POST,OPTIONS' 219 | headers = 'Origin, Accept, Content-Type, X-Requested-With, X-CSRF-Token' 220 | response.headers['Access-Control-Allow-Origin'] = '*' 221 | response.headers['Access-Control-Allow-Methods'] = methods 222 | response.headers['Access-Control-Allow-Headers'] = headers 223 | response.headers['Allow'] = methods 224 | 225 | def empty_response(self, *args, **kwargs): 226 | """Empty response.""" 227 | 228 | def get_bottle_app(self): 229 | """Return bottle instance.""" 230 | self.app = Bottle() 231 | self.dispatch_views() 232 | self.app.hook('after_request')(self.after_request) 233 | return self.app 234 | 235 | 236 | def apache(): 237 | """Run as WSGI application.""" 238 | v = Validator() 239 | return v.get_bottle_app() 240 | 241 | 242 | def main(): 243 | """Parse argument and run server when run from command line.""" 244 | parser = argparse.ArgumentParser(description=__doc__.strip(), 245 | formatter_class=argparse.ArgumentDefaultsHelpFormatter) 246 | 247 | parser.add_argument('--hostname', default='localhost', 248 | help='Hostname or IP address to bind to (use 0.0.0.0 for all)') 249 | parser.add_argument('--port', default=8080, type=int, 250 | help='Server port to bind to. Values below 1024 require root privileges.') 251 | 252 | args = parser.parse_args() 253 | 254 | v = Validator() 255 | 256 | run(host=args.hostname, port=args.port, app=v.get_bottle_app()) 257 | 258 | 259 | if __name__ == "__main__": 260 | main() 261 | else: 262 | application = apache() 263 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Presentation API Validator — IIIF | International Image Interoperability Framework 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 39 | 40 |
41 |
42 | Home 43 |
44 |
45 | 46 |
47 |
48 |
49 | 50 |
51 |

Presentation API Validator

52 |
53 | 54 |
55 |
56 | This service will validate a IIIF Presentation API resource against the specification. Fill in the URL 57 | of your manifest, and it will try to parse it and issue errors for failed requirements, and warnings for 58 | recommendations that haven't been followed. 59 |
60 | 61 |
62 |
63 | 64 | URL of Manifest to Validate:
65 |
66 | 67 | Select Presentation API Version: 68 | 74 |
75 | 76 |
77 | 78 | 79 |
80 |
81 | 82 | 87 | 88 |
89 |
90 | 91 |
92 | Technical Notes 93 |

94 | The Accept header option tells the validator to use content negotiation 95 | to retrieve a manifest at a given URL. This may be used to retrieve manifests from service 96 | providers that support content negotiation for switching between IIIF versions. 97 |

98 |

99 | If you would like to use the validator programatically, there are two options: 100 |

101 |
    102 |
  • Download the code from github and run it locally.
  • 103 |
  • Use it online with JSON based output, by an HTTP GET to this endpoint:
    104 | http://iiif.io/api/presentation/validator/service/validate?version=2.1&url=manifest-url-here&accept=true|false 105 |
  • 106 |
107 |
108 |
109 |
110 |
111 |
112 | 113 | 122 | 123 | 124 | 183 | 184 | 193 | 194 | 195 | 196 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | bottle 2 | iiif_prezi 3 | jsonschema 4 | mock 5 | jsonpath-rw 6 | requests 7 | -------------------------------------------------------------------------------- /runDocker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker build -t validator . && docker run -it --rm -p 8080:8080 --name validator validator:latest 4 | -------------------------------------------------------------------------------- /schema/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/presentation-validator/e38c1e6a5b8160327371b5419a97f9670d4266a9/schema/__init__.py -------------------------------------------------------------------------------- /schema/error_processor.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python3 2 | 3 | from jsonschema import Draft7Validator, RefResolver 4 | from jsonschema.exceptions import ValidationError, SchemaError, best_match, relevance 5 | from jsonpath_rw import jsonpath, parse 6 | import json 7 | import sys 8 | import re 9 | 10 | class IIIFErrorParser(object): 11 | """ 12 | This class tries to clean up json schema validation errors to remove 13 | errors that are misleading. Particularly ones where part of the error is related 14 | to a part of the schema with a different type. This occurs when you use `oneOf` and 15 | the validation then doesn't know if its valid or not so gives misleading errors. To clean this up 16 | this classes dismisses validation errors related to collections and annotation lists if the type is a Manifest. 17 | 18 | To initalise: 19 | 20 | errorParser = IIIFErrorParser(schema, iiif_json) 21 | 22 | where: 23 | schema: the schema as a JSON object 24 | iiif_json: the IIIF asset which failed validation 25 | 26 | then test if the error is related to the type of the IIIF asset: 27 | 28 | if errorParser.isValid(validationError.absolute_schema_path, validationError.absolute_path): 29 | """ 30 | 31 | def __init__(self, schema, iiif_asset): 32 | """ 33 | Intialize the IIIFErrorParse. Parameters: 34 | schema: the schema as a JSON object 35 | iiif_json: the IIIF asset which failed validation 36 | """ 37 | self.schema = schema 38 | self.iiif_asset = iiif_asset 39 | self.resolver = RefResolver.from_schema(self.schema) 40 | 41 | def isValid(self, error_path, IIIFJsonPath): 42 | """ 43 | This checks wheather the passed error path is valid for this iiif_json 44 | If the type doesn't match in the hirearchy then this error can 45 | be dismissed as misleading. 46 | 47 | Arguments: 48 | error_path (list of strings and ints): the path to the schema error 49 | e.g. [u'allOf', 1, u'oneOf', 2, u'allOf', 1, u'properties', u'type', u'pattern'] 50 | from validation_error.absolute_schema_path 51 | IIIFJsonPath (list of strings and ints): the path to the validation error 52 | in the IIIF Json file. e.g. [u'items', 0, u'items', 0, u'items', 0, u'body'] 53 | from validation_error.absolute_path 54 | """ 55 | return self.parse(error_path, self.schema, self.iiif_asset, IIIFJsonPath) 56 | 57 | def diagnoseWhichOneOf(self, error_path, IIIFJsonPath): 58 | """ 59 | Given a schema error that ends in oneOf the current json schema library 60 | will check all possibilities in oneOf and return validation error messages for each one 61 | This method will identify the real oneOf that causes the error by checking the type of 62 | each oneOf possibility and returning only the one that matches. 63 | 64 | Arguments: 65 | error_path: list of strings and ints which are the path to the error in the schema 66 | generated from validation_error.absolute_schema_path 67 | 68 | IIIF Json Path: list of strings and ints which gives the path to the failing part of the 69 | IIIF data. Generated from validation_error.absolute_path 70 | """ 71 | 72 | # convert the IIIF path from a list to an actual JSON object containing only 73 | # the JSON which has failed. 74 | path = parse(self.pathToJsonPath(IIIFJsonPath)) 75 | iiifJsonPart = path.find(self.iiif_asset)[0].value 76 | 77 | # Extract only the part of the schema that has failed and in practice will 78 | # be a list of the oneOf possibilities. Also add all references in the schema 79 | # so these resolve 80 | schema_part = self.addReferences(self.getSchemaPortion(error_path)) 81 | valid_errors = [] 82 | oneOfIndex = 0 83 | # For each of the oneOf possibilities in the current part of the schema 84 | for possibility in schema_part: 85 | try: 86 | # run through a validator passing the IIIF data snippet 87 | # and the json schema snippet 88 | validator = Draft7Validator(possibility) 89 | results = validator.iter_errors(iiifJsonPart) 90 | except SchemaError as err: 91 | print('Problem with the supplied schema:\n') 92 | print(err) 93 | raise 94 | 95 | # One of the oneOf possibilities is a reference to another part of the schema 96 | # this won't bother the validator but will complicate the diagnoise so replace 97 | # it with the actual schema json (and copy all references) 98 | if isinstance(possibility, dict) and "$ref" in possibility: 99 | tmpClas = possibility['classes'] 100 | tmpType = possibility['types'] 101 | possibility = self.resolver.resolve(possibility['$ref'])[1] 102 | possibility['classes'] = tmpClas 103 | possibility['types'] = tmpType 104 | 105 | # This oneOf possiblity failed validation 106 | if results: 107 | addErrors = True 108 | store_errs = [] 109 | for err in results: 110 | # For each of the reported errors check the types with the IIIF data to see if its relevant 111 | # if one error in this oneOf possibility group is not relevant then none a relevant so discard 112 | if not self.parse(list(err.absolute_schema_path), possibility, iiifJsonPart, list(err.absolute_path)): 113 | addErrors = False 114 | else: 115 | # if this oneOf possiblity is still relevant add it to the list and check 116 | # its not another oneOf error 117 | if addErrors: 118 | # if error is also a oneOf then diagnoise again 119 | #print ('Schema path: {} error path: {}'.format(err.absolute_schema_path, error_path)) 120 | if err.absolute_schema_path[-1] == 'oneOf' and err.absolute_schema_path != error_path and 'rights' not in err.absolute_schema_path: 121 | error_path.append(oneOfIndex) # this is is related to one of the original oneOfs at index oneOfIndex 122 | error_path.extend(err.absolute_schema_path) # but we found another oneOf test at this location 123 | result = (self.diagnoseWhichOneOf(error_path, IIIFJsonPath)) # so recursivly discovery real error 124 | 125 | if isinstance(result, ValidationError): 126 | store_errs.append(result) 127 | else: 128 | store_errs.extend(result) 129 | 130 | #print ('would add: {} by addErrors is {}'.format(err.message, addErrors)) 131 | else: 132 | store_errs.append(err) 133 | # if All errors are relevant to the current type add them to the list 134 | if addErrors: 135 | valid_errors += store_errs 136 | oneOfIndex += 1 137 | 138 | if valid_errors: 139 | # this may hide errors as we are only selecting the first one but better to get one to work on and then can re-run 140 | # Also need to convert back the error paths to the full path 141 | error_path.reverse() 142 | IIIFJsonPath.reverse() 143 | for error in valid_errors: 144 | error.absolute_schema_path.extendleft(error_path) 145 | error.absolute_path.extendleft(IIIFJsonPath) 146 | # print ('Err {}, path {}'.format(error.message, error.path)) 147 | return valid_errors 148 | else: 149 | # Failed to find the source of the error so most likely its a problem with the type 150 | # and it didn't match any of the possible oneOf types 151 | 152 | path = parse(self.pathToJsonPath(IIIFJsonPath)) 153 | instance = path.find(self.iiif_asset)[0].value 154 | IIIFJsonPath.append('type') 155 | #print (IIIFJsonPath) 156 | return ValidationError(message='Failed to find out which oneOf test matched your data. This is likely due to an issue with the type and it not being valid value at this level. SchemaPath: {}'.format(self.pathToJsonPath(error_path)), 157 | path=[], schema_path=error_path, schema=self.getSchemaPortion(error_path), instance=instance) 158 | 159 | 160 | 161 | def pathToJsonPath(self, pathAsList): 162 | """ 163 | Convert a json path as a list of keys and indexes to a json path 164 | 165 | Arguments: 166 | pathAsList e.g. [u'items', 0, u'items', 0, u'items', 0, u'body'] 167 | """ 168 | jsonPath = "$" 169 | for item in pathAsList: 170 | if isinstance(item, int): 171 | jsonPath += '[{}]'.format(item) 172 | else: 173 | jsonPath += '.{}'.format(item) 174 | return jsonPath 175 | 176 | def getSchemaPortion(self, schemaPath): 177 | """ 178 | Given the path return the relevant part of the schema 179 | Arguments: 180 | schemaPath: e.g. [u'allOf', 1, u'oneOf', 2, u'allOf', 1, u'properties', u'type', u'pattern'] 181 | """ 182 | schemaEl = self.schema 183 | for pathPart in schemaPath: 184 | try: 185 | if isinstance(schemaEl[pathPart], dict) and "$ref" in schemaEl[pathPart]: 186 | schemaEl = self.resolver.resolve(schemaEl[pathPart]['$ref'])[1] 187 | else: 188 | schemaEl = schemaEl[pathPart] 189 | except KeyError as error: 190 | # print (schemaEl) 191 | raise KeyError 192 | 193 | return schemaEl 194 | 195 | def addReferences(self, schemaPart): 196 | """ 197 | For the passed schemaPart add any references so that all #ref statements 198 | resolve in the schemaPart cut down schema. Note this currently is hardcoded to 199 | copy types and classes but could be more clever. 200 | """ 201 | definitions = {} 202 | definitions['types'] = self.schema['types'] 203 | definitions['classes'] = self.schema['classes'] 204 | for item in schemaPart: 205 | item.update(definitions) 206 | 207 | return schemaPart 208 | 209 | def parse(self, error_path, schemaEl, iiif_asset, IIIFJsonPath, parent=None, jsonPath="$"): 210 | """ 211 | Private method which recursivly travels the schema JSON to find 212 | type checks and performs them until it finds a mismatch. If it finds 213 | a mismatch it returns False. 214 | 215 | Parameters: 216 | error_path (list of strings and ints): the path to the schema error 217 | e.g. [u'allOf', 1, u'oneOf', 2, u'allOf', 1, u'properties', u'type', u'pattern'] 218 | from validation_error.absolute_schema_path 219 | schemaEl: the current element we are testing in this iteration. Start with the root 220 | parent: the last element tested 221 | jsonPath: the path in the IIIF assset that relates to the current item in the schema we are looking at 222 | IIIFJsonPath (list of strings and ints): the path to the validation error 223 | in the IIIF Json file. e.g. [u'items', 0, u'items', 0, u'items', 0, u'body'] 224 | from validation_error.absolute_path 225 | 226 | """ 227 | if len(error_path) <= 0: 228 | return True 229 | 230 | if isinstance(schemaEl, dict) and "$ref" in schemaEl: 231 | #print ('Found ref, trying to resolve: {}'.format(schemaEl['$ref'])) 232 | return self.parse(error_path, self.resolver.resolve(schemaEl['$ref'])[1], iiif_asset, IIIFJsonPath, parent, jsonPath) 233 | 234 | 235 | #print ("Path: {} ".format(error_path)) 236 | pathEl = error_path.pop(0) 237 | # Check current type to see if its a match 238 | if pathEl == 'type' and parent == 'properties': 239 | if 'pattern' in schemaEl['type']: 240 | value = schemaEl['type']['pattern'] 241 | elif 'const' in schemaEl['type']: 242 | value = schemaEl['type']['const'] 243 | elif 'oneOf' in schemaEl['type']: 244 | value = [] 245 | for option in schemaEl['type']['oneOf']: 246 | if 'pattern' in option: 247 | value.append(option['pattern']) 248 | elif 'const' in option: 249 | value.append(option['const']) 250 | #print ('Using values: {}'.format(value)) 251 | elif 'anyOf' in schemaEl['type']: 252 | value = [] 253 | for option in schemaEl['type']['anyOf']: 254 | if 'pattern' in option: 255 | value.append(option['pattern']) 256 | elif 'const' in option: 257 | value.append(option['const']) 258 | #print ('Using values: {}'.format(value)) 259 | 260 | if not self.isTypeMatch(jsonPath + '.type', iiif_asset, value, IIIFJsonPath): 261 | return False 262 | # Check child type to see if its a match 263 | elif pathEl == 'properties' and 'type' in schemaEl['properties'] and 'pattern' in schemaEl['properties']['type']: 264 | value = schemaEl['properties']['type']['pattern'] 265 | if not self.isTypeMatch(jsonPath + '.type', iiif_asset, value, IIIFJsonPath): 266 | return False 267 | # This is the case where additionalProperties has falied but need to check 268 | # if the type didn't match anyway 269 | elif pathEl == 'additionalProperties' and 'properties' in schemaEl and 'type' in schemaEl['properties'] and 'pattern' in schemaEl['properties']['type']: 270 | value = schemaEl['properties']['type']['pattern'] 271 | if not self.isTypeMatch(jsonPath + '.type', iiif_asset, value, IIIFJsonPath): 272 | return False 273 | # if there is a property called items which is of type array add an item array 274 | elif 'type' in schemaEl and schemaEl['type'] == 'array': 275 | jsonPath += '.{}[_]'.format(parent) 276 | #print (schemaEl) 277 | #print (jsonPath) 278 | # For all properties add json key but ignore items which are handled differently above 279 | elif parent == 'properties' and pathEl != 'items' and "ref" in schemaEl[pathEl]: 280 | # check type 281 | jsonPath += '.{}'.format(pathEl) 282 | #print (schemaEl) 283 | 284 | 285 | if isinstance(schemaEl[pathEl], dict) and "$ref" in schemaEl[pathEl]: 286 | #print ('Found ref, trying to resolve: {}'.format(schemaEl[pathEl]['$ref'])) 287 | return self.parse(error_path, self.resolver.resolve(schemaEl[pathEl]['$ref'])[1], iiif_asset, IIIFJsonPath, pathEl, jsonPath) 288 | else: 289 | return self.parse(error_path, schemaEl[pathEl], iiif_asset, IIIFJsonPath, pathEl, jsonPath) 290 | 291 | def isTypeMatch(self, iiifPath, iiif_asset, schemaType, IIIFJsonPath): 292 | """ 293 | Checks the required type in the schema with the actual type 294 | in the iiif_asset to see if it matches. 295 | Parameters: 296 | iiifPath: the json path in the iiif_asset to the type to be checked. Due to 297 | the way the schema works the index to arrays is left as _ e.g. 298 | $.items[_].items[_].items[_]. The indexes in the array are contained 299 | in IIIFJsonPath variable 300 | schemaType: the type from the schema that should match the type in the iiif_asset 301 | IIIFJsonPath: (Array of strings and int) path to the validation error in the iiif_asset 302 | e.g. [u'items', 0, u'items', 0, u'items', 0, u'body']. The indexes 303 | in this list are used to replace the _ indexes in the iiifPath 304 | 305 | Returns True if the schema type matches the iiif_asset type 306 | """ 307 | # get ints from IIIFJsonPath replace _ with numbers 308 | if IIIFJsonPath: 309 | indexes = [] 310 | for item in IIIFJsonPath: 311 | if isinstance(item, int): 312 | indexes.append(item) 313 | count = 0 314 | #print (iiifPath) 315 | indexDelta = 0 316 | for index in find(iiifPath, '_'): 317 | index += indexDelta 318 | #print ('Replacing {} with {}'.format(iiifPath[index], indexes[count])) 319 | iiifPath = iiifPath[:index] + str(indexes[count]) + iiifPath[index + 1:] 320 | # if you replace [_] with a number greater than 9 you are taking up two spaces in the 321 | # string so the index in the for loop starts to be off by one. Calculating the delta 322 | # sorts this out 323 | if len(str(indexes[count])) > 1: 324 | indexDelta += len(str(indexes[count])) -1 325 | count += 1 326 | 327 | #print ('JsonPath: {} IIIF Path {} type: {}'.format(iiifPath, IIIFJsonPath, schemaType)) 328 | path = parse(iiifPath) 329 | results = path.find(iiif_asset) 330 | #print ('Path: {} Results: '.format(path, results)) 331 | if not results: 332 | # type not found so return True as this maybe the correct error 333 | return True 334 | typeValue = results[0].value 335 | #print ('Found type {} and schemaType {}'.format(typeValue, schemaType)) 336 | if isinstance(schemaType, list): 337 | for typeOption in schemaType: 338 | #print ('Testing {} = {}'.format(typeOption, typeValue)) 339 | if re.match(typeOption, typeValue): 340 | return True 341 | return False 342 | else: 343 | return re.match(schemaType, typeValue) 344 | 345 | def find(str, ch): 346 | """ 347 | Used to create an list with the indexes of a particular character. e.g.: 348 | find('o_o_o','_') = [1,3] 349 | """ 350 | for i, ltr in enumerate(str): 351 | if ltr == ch: 352 | yield i 353 | 354 | if __name__ == '__main__': 355 | if len(sys.argv) != 2: 356 | print ('Usage:\n\t{} manifest'.format(sys.argv[0])) 357 | exit(-1) 358 | 359 | with open(sys.argv[1]) as json_file: 360 | print ('Loading: {}'.format(sys.argv[1])) 361 | try: 362 | iiif_json = json.load(json_file) 363 | except ValueError as err: 364 | print ('Failed to load JSON due to: {}'.format(err)) 365 | exit(-1) 366 | schema_file = 'schema/iiif_3_0.json' 367 | with open(schema_file) as json_file: 368 | print ('Loading: {}'.format(schema_file)) 369 | try: 370 | schema = json.load(json_file) 371 | except ValueError as err: 372 | print ('Failed to load JSON due to: {}'.format(err)) 373 | exit(-1) 374 | errorParser = IIIFErrorParser(schema, iiif_json) 375 | 376 | # annotationPage 377 | path = [u'allOf', 1, u'oneOf', 2, u'allOf', 1, u'properties', u'type', u'pattern'] 378 | print("Path: '{}' is valid: {}".format(path, errorParser.isValid(path))) 379 | # Annotation 380 | path = [u'allOf', 1, u'oneOf', 2, u'allOf', 1, u'properties', u'items', u'items', u'allOf', 1, u'properties', u'type', u'pattern'] 381 | print("Path: '{}' is valid: {}".format(path, errorParser.isValid(path))) 382 | # Additional props fail 383 | path = [u'allOf', 1, u'oneOf', 2, u'allOf', 1, u'additionalProperties'] 384 | print("Path: '{}' is valid: {}".format(path, errorParser.isValid(path))) 385 | # Collection 386 | path = [u'allOf', 1, u'oneOf', 1, u'allOf', 1, u'properties', u'thumbnail', u'items', u'oneOf'] 387 | print("Collection Path: '{}' is valid: {}".format(path, errorParser.isValid(path))) 388 | # Collection 2 389 | path = [u'allOf', 1, u'oneOf', 1, u'allOf', 1, u'properties', u'type', u'pattern'] 390 | print("Collection 2 Path: '{}' is valid: {}".format(path, errorParser.isValid(path))) 391 | # Collection 3 392 | path = [u'allOf', 1, u'oneOf', 1, u'allOf', 1, u'properties', u'items', u'items', u'oneOf'] 393 | print("Collection 3 Path: '{}' is valid: {}".format(path, errorParser.isValid(path))) 394 | # success service 395 | path = [u'allOf', 1, u'oneOf', 0, u'allOf', 1, u'properties', u'thumbnail', u'items', u'oneOf'] 396 | print("Success Service Path: '{}' is valid: {}".format(path, errorParser.isValid(path))) 397 | # Success service in canvas 398 | path = [u'allOf', 1, u'oneOf', 0, u'allOf', 1, u'properties', u'items', u'items', u'allOf', 1, u'properties', u'items', u'items', u'allOf', 1, u'properties', u'items', u'items', u'allOf', 1, u'properties', u'body', u'oneOf'] 399 | print("Success Service Canvas Path: '{}' is valid: {}".format(path, errorParser.isValid(path, [u'items', 0, u'items', 0, u'items', 0, u'body']))) 400 | -------------------------------------------------------------------------------- /schema/iiif_3_0.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema#", 3 | "$comment": "IIIF basic types", 4 | "types": { 5 | "id": { 6 | "type": "string", 7 | "format": "uri", 8 | "pattern": "^http.*$", 9 | "title": "Id must be present and must be a URI" 10 | }, 11 | "lngString": { 12 | "title": "Language string, must have a language and value must be an array.", 13 | "type": "object", 14 | "patternProperties": { 15 | "^[a-zA-Z-][a-zA-Z-]*$": { 16 | "type": "array", 17 | "items": { "type": "string"} 18 | }, 19 | "^none$": { 20 | "type": "array", 21 | "items": { "type": "string"} 22 | } 23 | }, 24 | "additionalProperties": false 25 | }, 26 | "dimension": { 27 | "type": "integer", 28 | "exclusiveMinimum": 0 29 | }, 30 | "keyValueString": { 31 | "type": "object", 32 | "properties": { 33 | "label": {"$ref": "#/types/lngString" }, 34 | "value": {"$ref": "#/types/lngString" } 35 | }, 36 | "required": ["label", "value"] 37 | }, 38 | "BCP47": { 39 | "anyOf": [ 40 | { 41 | "type":"string", 42 | "pattern": "^[a-zA-Z-][a-zA-Z-]*$" 43 | }, 44 | { 45 | "type":"string", 46 | "pattern": "^none$" 47 | } 48 | ] 49 | }, 50 | "format": { 51 | "type": "string", 52 | "pattern": "^[a-z][a-z]*/.*$" 53 | }, 54 | "class": { 55 | "title": "Classes MUST have an id and type property and MAY have a label.", 56 | "type": "object", 57 | "properties": { 58 | "id": { "$ref": "#/types/id" }, 59 | "type": { "type": "string" }, 60 | "label": { "$ref": "#/types/lngString" } 61 | }, 62 | "required": ["id", "type"] 63 | }, 64 | "duration": { 65 | "type": "number", 66 | "exclusiveMinimum": 0 67 | }, 68 | "external": { 69 | "type": "array", 70 | "items": { 71 | "allOf": [ 72 | { "$ref": "#/types/class" }, 73 | { 74 | "type": "object", 75 | "properties": { 76 | "format": { "$ref": "#/types/format" }, 77 | "profile": { 78 | "type": "string" 79 | } 80 | } 81 | } 82 | ] 83 | } 84 | }, 85 | "reference": { 86 | "type": "object", 87 | "additionalProperties": true, 88 | "title": "Reference", 89 | "description":"Id. type, label but not items are required", 90 | "properties": { 91 | "id": { "$ref": "#/types/id" }, 92 | "label": {"$ref": "#/types/lngString" }, 93 | "type": { 94 | "type": "string", 95 | "pattern": "^Manifest$|^AnnotationPage$|^Collection$|^AnnotationCollection$|^Canvas$|^Range$" 96 | }, 97 | "thumbnail": { 98 | "type": "array", 99 | "items": { "$ref": "#/classes/resource" } 100 | } 101 | }, 102 | "required": ["id", "type"], 103 | "not": { "required": [ "items" ] } 104 | } 105 | }, 106 | 107 | "$comment": "IIIF Classes", 108 | "classes": { 109 | "metadata": { 110 | "type": "array", 111 | "items": { 112 | "$ref": "#/types/keyValueString" 113 | } 114 | }, 115 | "homepage": { 116 | "type": "array", 117 | "items": { 118 | "allOf": [ 119 | { "$ref": "#/types/class" }, 120 | { 121 | "type": "object", 122 | "properties": { 123 | "format": { "$ref": "#/types/format" }, 124 | "language": { 125 | "type": "array", 126 | "items": { "$ref": "#/types/BCP47" } 127 | } 128 | } 129 | } 130 | ] 131 | } 132 | }, 133 | "seeAlso": { 134 | "$ref": "#/types/external" 135 | }, 136 | "partOf": { 137 | "type": "array", 138 | "items": { 139 | "$ref": "#/types/class" 140 | } 141 | }, 142 | "choice": { 143 | "type": "object", 144 | "properties":{ 145 | "type": { 146 | "type": "string", 147 | "const": "Choice" 148 | }, 149 | "items":{ 150 | "type": "array" 151 | } 152 | }, 153 | "required": ["type", "items"] 154 | }, 155 | "resource": { 156 | "oneOf": [ 157 | { 158 | "title": "Annotation bodies MUST have an id and type property.", 159 | "type": "object", 160 | "properties": { 161 | "id": { "$ref": "#/types/id" }, 162 | "type": { 163 | "type": "string" 164 | }, 165 | "height": { "$ref": "#/types/dimension" }, 166 | "width": { "$ref": "#/types/dimension" }, 167 | "duration": { "$ref": "#/types/duration" }, 168 | "language": { "type": "string"}, 169 | "rendering": { "$ref": "#/types/external" }, 170 | "service": { "$ref": "#/classes/service" }, 171 | "format": { "$ref": "#/types/format" }, 172 | "label": {"$ref": "#/types/lngString" }, 173 | "thumbnail": { 174 | "type": "array", 175 | "items": { "$ref": "#/classes/resource" } 176 | }, 177 | "annotations": { 178 | "type": "array", 179 | "items": { 180 | "oneOf": [ 181 | { "$ref": "#/classes/annotationPage" }, 182 | { "$ref": "#/classes/annotationPageRef"} 183 | ] 184 | } 185 | } 186 | }, 187 | "required": ["id", "type"] 188 | }, 189 | { 190 | "title": "Annotation bodies which are TextualBody MUST have an type and value property.", 191 | "type": "object", 192 | "properties": { 193 | "id": { "$ref": "#/types/id" }, 194 | "type": { 195 | "type": "string", 196 | "pattern": "^TextualBody$", 197 | "default": "TextualBody" 198 | }, 199 | "value": { "type": "string" }, 200 | "format": { "$ref": "#/types/format" }, 201 | "language": { "type": "string"} 202 | }, 203 | "required": ["value", "type"] 204 | } 205 | ] 206 | }, 207 | "imgSvr": { 208 | "allOf": [ 209 | { "$ref": "#/classes/service" }, 210 | { 211 | "properties": { 212 | "profile": { "type": "string" }, 213 | "@id": { "$ref": "#/types/id" }, 214 | "@type": { "type": "string" } 215 | } 216 | } 217 | ] 218 | }, 219 | "service": { 220 | "type": "array", 221 | "items": { 222 | "oneOf": [ 223 | { 224 | "allOf": [ 225 | { "$ref": "#/types/class" }, 226 | { 227 | "type": "object", 228 | "properties": { 229 | "profile": { "type": "string" }, 230 | "service": { "$ref": "#/classes/service" } 231 | } 232 | } 233 | ] 234 | }, 235 | { 236 | "type": "object", 237 | "properties": { 238 | "@id": { "$ref": "#/types/id" }, 239 | "@type": { "type": "string" }, 240 | "profile": { "type": "string" }, 241 | "service": { "$ref": "#/classes/service" } 242 | }, 243 | "required": ["@id", "@type"] 244 | } 245 | ] 246 | } 247 | }, 248 | "rights": { 249 | "title": "Rights URI isn't from either Creative Commons or RightsStatements.org. Both require http links.", 250 | "oneOf": [ 251 | { 252 | "type": "string", 253 | "format": "uri", 254 | "pattern": "http://creativecommons.org/licenses/.*" 255 | }, 256 | { 257 | "type": "string", 258 | "format": "uri", 259 | "pattern": "http://creativecommons.org/publicdomain/.*" 260 | }, 261 | { 262 | "type": "string", 263 | "format": "uri", 264 | "pattern": "http://rightsstatements.org/vocab/.*" 265 | } 266 | ] 267 | }, 268 | "navDate": { 269 | "type": "string", 270 | "format": "date-time" 271 | }, 272 | "navPlace": { 273 | "type": "object", 274 | "properties": { 275 | "id": { "$ref": "#/types/id" }, 276 | "type": { 277 | "type": "string", 278 | "default": "FeatureCollection" 279 | }, 280 | "features": { 281 | "type": "array", 282 | "items": { 283 | "type": "object" 284 | } 285 | } 286 | }, 287 | "required": ["type"] 288 | }, 289 | "viewingDirection": { 290 | "anyOf": [ 291 | { 292 | "type": "string", 293 | "pattern": "^left-to-right$" 294 | }, 295 | { 296 | "type": "string", 297 | "pattern": "^right-to-left$" 298 | }, 299 | { 300 | "type": "string", 301 | "pattern": "^top-to-bottom$" 302 | }, 303 | { 304 | "type": "string", 305 | "pattern": "^bottom-to-top$" 306 | } 307 | ] 308 | }, 309 | "behavior": { 310 | "type": "array", 311 | "items": { 312 | "anyOf": [ 313 | { 314 | "type": "string", 315 | "pattern": "^auto-advance$" 316 | }, 317 | { 318 | "type": "string", 319 | "pattern": "^no-auto-advance$" 320 | }, 321 | { 322 | "type": "string", 323 | "pattern": "^repeat$" 324 | }, 325 | { 326 | "type": "string", 327 | "pattern": "^no-repeat$" 328 | }, 329 | { 330 | "type": "string", 331 | "pattern": "^unordered$" 332 | }, 333 | { 334 | "type": "string", 335 | "pattern": "^individuals$" 336 | }, 337 | { 338 | "type": "string", 339 | "pattern": "^continuous$" 340 | }, 341 | { 342 | "type": "string", 343 | "pattern": "^paged$" 344 | }, 345 | { 346 | "type": "string", 347 | "pattern": "^facing-pages$" 348 | }, 349 | { 350 | "type": "string", 351 | "pattern": "^non-paged$" 352 | }, 353 | { 354 | "type": "string", 355 | "pattern": "^multi-part$" 356 | }, 357 | { 358 | "type": "string", 359 | "pattern": "^together$" 360 | }, 361 | { 362 | "type": "string", 363 | "pattern": "^sequence$" 364 | }, 365 | { 366 | "type": "string", 367 | "pattern": "^thumbnail-nav$" 368 | }, 369 | { 370 | "type": "string", 371 | "pattern": "^no-nav$" 372 | }, 373 | { 374 | "type": "string", 375 | "pattern": "^hidden$" 376 | } 377 | ] 378 | } 379 | }, 380 | "provider": { 381 | "type": "array", 382 | "items": { 383 | "allOf": [ 384 | { "$ref": "#/types/class" }, 385 | { 386 | "type": "object", 387 | "properties": { 388 | "type": { 389 | "type": "string", 390 | "pattern": "^Agent$", 391 | "default": "Agent" 392 | }, 393 | "homepage": { "$ref": "#/classes/homepage" }, 394 | "logo": { 395 | "type": "array", 396 | "items": { "$ref": "#/classes/resource" } 397 | }, 398 | "seeAlso": { "$ref": "#/classes/seeAlso" } 399 | } 400 | } 401 | ] 402 | } 403 | }, 404 | "collection": { 405 | "allOf": [ 406 | { "$ref": "#/types/class" }, 407 | { 408 | "type": "object", 409 | "properties": { 410 | "type": { 411 | "type": "string", 412 | "pattern": "^Collection", 413 | "default": "Collection", 414 | "title": "Are you validating a collection?", 415 | "description":"If you are validating a manifest, you may get this error if there are errors in the manifest. The validator first validates it as a manifest and if that fails it will try and validate it using the other types." 416 | }, 417 | "metadata": { "$ref": "#/classes/metadata" }, 418 | "summary": { "$ref": "#/types/lngString" }, 419 | "requiredStatement": { "$ref": "#/types/keyValueString" }, 420 | "rendering": { "$ref": "#/types/external" }, 421 | "rights": { "$ref": "#/classes/rights" }, 422 | "navDate": { "$ref": "#/classes/navDate" }, 423 | "navPlace": { "$ref": "#/classes/navPlace" }, 424 | "provider": { "$ref": "#/classes/provider" }, 425 | "seeAlso": { "$ref": "#/classes/seeAlso" }, 426 | "services": { "$ref": "#/classes/service" }, 427 | "service": { "$ref": "#/classes/service" }, 428 | "placeholderCanvas": { "$ref": "#/classes/placeholderCanvas" }, 429 | "accompanyingCanvas": { "$ref": "#/classes/accompanyingCanvas" }, 430 | "thumbnail": { 431 | "type": "array", 432 | "items": { "$ref": "#/classes/resource" } 433 | }, 434 | "homepage": { "$ref": "#/classes/homepage" }, 435 | "behavior": { "$ref": "#/classes/behavior" }, 436 | "partOf": { "$ref": "#/classes/partOf" }, 437 | "items": { 438 | "type": "array", 439 | "items": { 440 | "anyOf": [ 441 | { "$ref": "#/classes/manifestRef" }, 442 | { "$ref": "#/classes/collectionRef" }, 443 | { "$ref": "#/classes/collection" } 444 | ] 445 | } 446 | }, 447 | "annotations": { 448 | "type": "array", 449 | "items": { 450 | "oneOf": [ 451 | { "$ref": "#/classes/annotationPage" }, 452 | { "$ref": "#/classes/annotationPageRef"} 453 | ] 454 | } 455 | } 456 | }, 457 | "required": ["id", "type", "label"] 458 | } 459 | ] 460 | }, 461 | "manifest": { 462 | "allOf": [ 463 | { "$ref": "#/types/class" }, 464 | { 465 | "type": "object", 466 | "additionalProperties": false, 467 | "properties": { 468 | "@context": { 469 | "oneOf": [ 470 | { 471 | "type": "array", 472 | "items": { 473 | "type": "string", 474 | "format": "uri", 475 | "pattern": "^http.*$" 476 | } 477 | }, 478 | { 479 | "type": "string", 480 | "const": "http://iiif.io/api/presentation/3/context.json" 481 | } 482 | ] 483 | }, 484 | "id": { "$ref": "#/types/id" }, 485 | "label": {"$ref": "#/types/lngString" }, 486 | "type": { 487 | "type": "string", 488 | "pattern": "^Manifest", 489 | "default": "Manifest" 490 | }, 491 | "metadata": { "$ref": "#/classes/metadata" }, 492 | "summary": { "$ref": "#/types/lngString" }, 493 | "requiredStatement": { "$ref": "#/types/keyValueString" }, 494 | "rendering": { "$ref": "#/types/external" }, 495 | "service": { "$ref": "#/classes/service" }, 496 | "services": { "$ref": "#/classes/service" }, 497 | "viewingDirection": { "$ref": "#/classes/viewingDirection" }, 498 | "placeholderCanvas": { "$ref": "#/classes/placeholderCanvas" }, 499 | "accompanyingCanvas": { "$ref": "#/classes/accompanyingCanvas" }, 500 | "rights": { "$ref": "#/classes/rights" }, 501 | "start": {}, 502 | "navDate": { "$ref": "#/classes/navDate" }, 503 | "navPlace": { "$ref": "#/classes/navPlace" }, 504 | "provider": { "$ref": "#/classes/provider" }, 505 | "seeAlso": { "$ref": "#/classes/seeAlso" }, 506 | "thumbnail": { 507 | "type": "array", 508 | "items": { "$ref": "#/classes/resource" } 509 | }, 510 | "homepage": { "$ref": "#/classes/homepage" }, 511 | "behavior": { "$ref": "#/classes/behavior" }, 512 | "partOf": { "$ref": "#/classes/partOf" }, 513 | "items": { 514 | "type": "array", 515 | "items": { 516 | "$ref": "#/classes/canvas" 517 | } 518 | }, 519 | "structures": { 520 | "type": "array", 521 | "items": { 522 | "$ref": "#/classes/range" 523 | } 524 | }, 525 | "annotations": { 526 | "type": "array", 527 | "items": { 528 | "oneOf": [ 529 | { "$ref": "#/classes/annotationPage" }, 530 | { "$ref": "#/classes/annotationPageRef"} 531 | ] 532 | } 533 | } 534 | }, 535 | "required": ["id", "type", "label"] 536 | } 537 | ] 538 | }, 539 | "manifestRef": { 540 | "allOf": [ 541 | { "$ref": "#/types/reference" }, 542 | { 543 | "type": "object", 544 | "properties": { 545 | "type": { 546 | "type": "string", 547 | "pattern": "^Manifest$", 548 | "default": "Manifest" 549 | }, 550 | "label": {"$ref": "#/types/lngString" } 551 | }, 552 | "required":["label"] 553 | } 554 | ] 555 | }, 556 | "collectionRef": { 557 | "allOf": [ 558 | { "$ref": "#/types/reference" }, 559 | { 560 | "type": "object", 561 | "properties": { 562 | "type": { 563 | "type": "string", 564 | "pattern": "^Collection$" 565 | }, 566 | "label": {"$ref": "#/types/lngString" } 567 | }, 568 | "required":["label"] 569 | } 570 | ] 571 | }, 572 | "rangeRef": { 573 | "allOf": [ 574 | { "$ref": "#/types/reference" }, 575 | { 576 | "type": "object", 577 | "properties": { 578 | "type": { 579 | "type": "string", 580 | "pattern": "^Range$" 581 | } 582 | } 583 | } 584 | ] 585 | }, 586 | "canvasRef": { 587 | "allOf": [ 588 | { "$ref": "#/types/reference" }, 589 | { 590 | "type": "object", 591 | "properties": { 592 | "type": { 593 | "type": "string", 594 | "pattern": "^Canvas$" 595 | } 596 | } 597 | } 598 | ] 599 | }, 600 | "annotationPageRef": { 601 | "oneOf": [ 602 | { 603 | "type": "string" 604 | }, 605 | { 606 | "allOf": [ 607 | { "$ref": "#/types/reference" }, 608 | { 609 | "type": "object", 610 | "properties": { 611 | "type": { 612 | "type": "string", 613 | "pattern": "^AnnotationPage$" 614 | } 615 | } 616 | } 617 | ] 618 | } 619 | ] 620 | }, 621 | "annotationCollectionRef": { 622 | "oneOf": [ 623 | { 624 | "type": "string" 625 | }, 626 | { 627 | "allOf": [ 628 | { "$ref": "#/types/reference" }, 629 | { 630 | "type": "object", 631 | "properties": { 632 | "type": { 633 | "type": "string", 634 | "pattern": "^AnnotationCollection$" 635 | } 636 | } 637 | } 638 | ] 639 | } 640 | ] 641 | }, 642 | "canvas": { 643 | "allOf": [ 644 | { "$ref": "#/types/class" }, 645 | { 646 | "type": "object", 647 | "properties": { 648 | "type": { 649 | "type": "string", 650 | "pattern": "^Canvas$", 651 | "default": "Canvas" 652 | }, 653 | "height": { "$ref": "#/types/dimension" }, 654 | "width": { "$ref": "#/types/dimension" }, 655 | "duration": { "$ref": "#/types/duration" }, 656 | "metadata": { "$ref": "#/classes/metadata" }, 657 | "summary": { "$ref": "#/types/lngString" }, 658 | "requiredStatement": { "$ref": "#/types/keyValueString" }, 659 | "rendering": { "$ref": "#/types/external" }, 660 | "rights": { "$ref": "#/classes/rights" }, 661 | "navDate": { "$ref": "#/classes/navDate" }, 662 | "navPlace": { "$ref": "#/classes/navPlace" }, 663 | "provider": { "$ref": "#/classes/provider" }, 664 | "seeAlso": { "$ref": "#/classes/seeAlso" }, 665 | "service": { "$ref": "#/classes/service" }, 666 | "placeholderCanvas": { "$ref": "#/classes/placeholderCanvas" }, 667 | "accompanyingCanvas": { "$ref": "#/classes/accompanyingCanvas" }, 668 | "thumbnail": { 669 | "type": "array", 670 | "items": { "$ref": "#/classes/resource" } 671 | }, 672 | "homepage": { "$ref": "#/classes/homepage" }, 673 | "behavior": { "$ref": "#/classes/behavior" }, 674 | "partOf": { "$ref": "#/classes/partOf" }, 675 | "items": { 676 | "type": "array", 677 | "items": { 678 | "$ref": "#/classes/annotationPage" 679 | } 680 | }, 681 | "annotations": { 682 | "type": "array", 683 | "items": { 684 | "oneOf": [ 685 | { "$ref": "#/classes/annotationPage" }, 686 | { "$ref": "#/classes/annotationPageRef"} 687 | ] 688 | } 689 | } 690 | }, 691 | "required": ["items"], 692 | "anyOf":[ 693 | { "required": ["width"] }, 694 | { "required": ["height"] }, 695 | { "required": ["duration"] } 696 | ], 697 | "dependencies": { 698 | "width": ["height"], 699 | "height": ["width"] 700 | } 701 | } 702 | ] 703 | }, 704 | "placeholderCanvas": { 705 | "allOf": [ 706 | { "$ref": "#/types/class" }, 707 | { 708 | "type": "object", 709 | "properties": { 710 | "type": { 711 | "type": "string", 712 | "pattern": "^Canvas$", 713 | "default": "Canvas" 714 | }, 715 | "height": { "$ref": "#/types/dimension" }, 716 | "width": { "$ref": "#/types/dimension" }, 717 | "duration": { "$ref": "#/types/duration" }, 718 | "metadata": { "$ref": "#/classes/metadata" }, 719 | "summary": { "$ref": "#/types/lngString" }, 720 | "requiredStatement": { "$ref": "#/types/keyValueString" }, 721 | "rendering": { "$ref": "#/types/external" }, 722 | "rights": { "$ref": "#/classes/rights" }, 723 | "navDate": { "$ref": "#/classes/navDate" }, 724 | "navPlace": { "$ref": "#/classes/navPlace" }, 725 | "provider": { "$ref": "#/classes/provider" }, 726 | "seeAlso": { "$ref": "#/classes/seeAlso" }, 727 | "service": { "$ref": "#/classes/service" }, 728 | "thumbnail": { 729 | "type": "array", 730 | "items": { "$ref": "#/classes/resource" } 731 | }, 732 | "homepage": { "$ref": "#/classes/homepage" }, 733 | "behavior": { "$ref": "#/classes/behavior" }, 734 | "partOf": { "$ref": "#/classes/partOf" }, 735 | "items": { 736 | "type": "array", 737 | "items": { 738 | "$ref": "#/classes/annotationPage" 739 | } 740 | }, 741 | "annotations": { 742 | "type": "array", 743 | "items": { 744 | "oneOf": [ 745 | { "$ref": "#/classes/annotationPage" }, 746 | { "$ref": "#/classes/annotationPageRef"} 747 | ] 748 | } 749 | } 750 | }, 751 | "required": ["items"], 752 | "anyOf":[ 753 | { "required": ["width"] }, 754 | { "required": ["height"] }, 755 | { "required": ["duration"] } 756 | ], 757 | "dependencies": { 758 | "width": ["height"], 759 | "height": ["width"] 760 | }, 761 | "not": { "required": [ "placeholderCanvas", "accompanyingCanvas" ] } 762 | } 763 | ] 764 | }, 765 | "accompanyingCanvas": { 766 | "allOf": [ 767 | { "$ref": "#/types/class" }, 768 | { 769 | "type": "object", 770 | "properties": { 771 | "type": { 772 | "type": "string", 773 | "pattern": "^Canvas$", 774 | "default": "Canvas" 775 | }, 776 | "height": { "$ref": "#/types/dimension" }, 777 | "width": { "$ref": "#/types/dimension" }, 778 | "duration": { "$ref": "#/types/duration" }, 779 | "metadata": { "$ref": "#/classes/metadata" }, 780 | "summary": { "$ref": "#/types/lngString" }, 781 | "requiredStatement": { "$ref": "#/types/keyValueString" }, 782 | "rendering": { "$ref": "#/types/external" }, 783 | "rights": { "$ref": "#/classes/rights" }, 784 | "navDate": { "$ref": "#/classes/navDate" }, 785 | "navPlace": { "$ref": "#/classes/navPlace" }, 786 | "provider": { "$ref": "#/classes/provider" }, 787 | "seeAlso": { "$ref": "#/classes/seeAlso" }, 788 | "service": { "$ref": "#/classes/service" }, 789 | "thumbnail": { 790 | "type": "array", 791 | "items": { "$ref": "#/classes/resource" } 792 | }, 793 | "homepage": { "$ref": "#/classes/homepage" }, 794 | "behavior": { "$ref": "#/classes/behavior" }, 795 | "partOf": { "$ref": "#/classes/partOf" }, 796 | "items": { 797 | "type": "array", 798 | "items": { 799 | "$ref": "#/classes/annotationPage" 800 | } 801 | }, 802 | "annotations": { 803 | "type": "array", 804 | "items": { 805 | "oneOf": [ 806 | { "$ref": "#/classes/annotationPage" }, 807 | { "$ref": "#/classes/annotationPageRef"} 808 | ] 809 | } 810 | } 811 | }, 812 | "required": ["items"], 813 | "anyOf":[ 814 | { "required": ["width"] }, 815 | { "required": ["height"] }, 816 | { "required": ["duration"] } 817 | ], 818 | "dependencies": { 819 | "width": ["height"], 820 | "height": ["width"] 821 | }, 822 | "not": { "required": [ "placeholderCanvas", "accompanyingCanvas" ] } 823 | } 824 | ] 825 | }, 826 | "annotationCollection": { 827 | "allOf": [ 828 | { "$ref": "#/types/class" }, 829 | { 830 | "type": "object", 831 | "properties": { 832 | "type": { 833 | "type": "string", 834 | "pattern": "^AnnotationCollection$", 835 | "default": "AnnotationCollection" 836 | }, 837 | "rendering": { "$ref": "#/types/external" }, 838 | "partOf": { "$ref": "#/classes/partOf" }, 839 | "next": { "$ref": "#/classes/annotationPageRef" }, 840 | "first": { "$ref": "#/classes/annotationPageRef" }, 841 | "last": { "$ref": "#/classes/annotationPageRef" }, 842 | "service": { "$ref": "#/classes/service" }, 843 | "total": { 844 | "type": "integer", 845 | "exclusiveMinimum": 0 846 | }, 847 | "thumbnail": { 848 | "type": "array", 849 | "items": { "$ref": "#/classes/resource" } 850 | }, 851 | "items": { 852 | "type": "array", 853 | "items": { 854 | "$ref": "#/classes/annotation" 855 | } 856 | } 857 | }, 858 | "required": ["items"] 859 | } 860 | ] 861 | }, 862 | "annotationPage": { 863 | "allOf": [ 864 | { "$ref": "#/types/class" }, 865 | { 866 | "type": "object", 867 | "title": "AnnotationPage", 868 | "description":"id, type and items required", 869 | "properties": { 870 | "@context": { 871 | "oneOf": [ 872 | { 873 | "type": "array", 874 | "items": { 875 | "type": "string", 876 | "format": "uri", 877 | "pattern": "^http.*$" 878 | } 879 | }, 880 | { 881 | "type": "string", 882 | "const": "http://iiif.io/api/presentation/3/context.json" 883 | } 884 | ] 885 | }, 886 | "id": { "$ref": "#/types/id" }, 887 | "type": { 888 | "type": "string", 889 | "pattern": "^AnnotationPage$", 890 | "default": "AnnotationPage" 891 | }, 892 | "rendering": { "$ref": "#/types/external" }, 893 | "label": {"$ref": "#/types/lngString" }, 894 | "service": { "$ref": "#/classes/service" }, 895 | "thumbnail": { 896 | "type": "array", 897 | "items": { "$ref": "#/classes/resource" } 898 | }, 899 | "items": { 900 | "type": "array", 901 | "items": { 902 | "$ref": "#/classes/annotation" 903 | } 904 | }, 905 | "partOf": { 906 | "type": "array", 907 | "items": { 908 | "oneOf": [ 909 | { "$ref": "#/classes/annotationCollection" }, 910 | { "$ref": "#/classes/annotationCollectionRef" } 911 | ] 912 | } 913 | }, 914 | "next": { "$ref": "#/classes/annotationPageRef" }, 915 | "prev": { "$ref": "#/classes/annotationPageRef" }, 916 | "first": { "$ref": "#/classes/annotationPageRef" }, 917 | "last": { "$ref": "#/classes/annotationPageRef" } 918 | }, 919 | "required": ["id","type","items"], 920 | "additionalProperties": false 921 | } 922 | ] 923 | }, 924 | "annotation": { 925 | "allOf": [ 926 | { "$ref": "#/types/class" }, 927 | { 928 | "type": "object", 929 | "properties": { 930 | "type": { 931 | "type": "string", 932 | "pattern": "^Annotation$", 933 | "default": "Annotation" 934 | }, 935 | "service": { "$ref": "#/classes/service" }, 936 | "rendering": { "$ref": "#/types/external" }, 937 | "thumbnail": { 938 | "type": "array", 939 | "items": { "$ref": "#/classes/resource" } 940 | }, 941 | "motivation": { 942 | "oneOf": [ 943 | { "type": "string" }, 944 | { 945 | "type": "array", 946 | "items": { 947 | "type": "string" 948 | } 949 | } 950 | ] 951 | }, 952 | "body": { 953 | "anyOf": [ 954 | { 955 | "type": "object", 956 | "$ref": "#/classes/resource" 957 | }, 958 | { 959 | "type": "object", 960 | "allOf":[ 961 | { "$ref": "#/classes/choice" }, 962 | { 963 | "properties": { 964 | "items": { 965 | "type": "array", 966 | "items": {"$ref": "#/classes/resource"} 967 | } 968 | }, 969 | "required": ["items"] 970 | } 971 | ] 972 | }, 973 | { 974 | "type": "array", 975 | "items": { 976 | "type": "object" 977 | } 978 | } 979 | 980 | ] 981 | }, 982 | "target": { 983 | "anyOf": [ 984 | { "$ref": "#/classes/annoTarget" }, 985 | { 986 | "type": "array", 987 | "items": { 988 | "$ref": "#/classes/annoTarget" 989 | } 990 | } 991 | ] 992 | } 993 | }, 994 | "required": ["id", "target", "type"] 995 | } 996 | ] 997 | }, 998 | "annoTarget": { 999 | "oneOf": [ 1000 | { 1001 | "type": "string", 1002 | "format": "uri", 1003 | "pattern": "^http.*$" 1004 | }, 1005 | { 1006 | "$ref": "#/classes/specificResource" 1007 | } 1008 | ] 1009 | }, 1010 | "specificResource": { 1011 | "type": "object", 1012 | "properties": { 1013 | "id": { "$ref": "#/types/id" }, 1014 | "type": { 1015 | "type": "string", 1016 | "pattern": "^SpecificResource$", 1017 | "default": "SpecificResource" 1018 | }, 1019 | "format": { "$ref": "#/types/format" }, 1020 | "accessibility": { "type": "string"}, 1021 | "source": { 1022 | "oneOf": [ 1023 | { "$ref": "#/types/id" }, 1024 | { "$ref": "#/types/class" } 1025 | ] 1026 | }, 1027 | "scope": { "$ref": "#/types/id"}, 1028 | "selector": { 1029 | "oneOf": [ 1030 | { "$ref": "#/classes/selector" }, 1031 | { 1032 | "type": "array", 1033 | "items": { 1034 | "$ref": "#/classes/selector" 1035 | } 1036 | } 1037 | ] 1038 | } 1039 | }, 1040 | "required": ["source"] 1041 | }, 1042 | "selector": { 1043 | "oneOf": [ 1044 | { 1045 | "type": "string", 1046 | "format": "uri", 1047 | "pattern": "^http.*$" 1048 | }, 1049 | { 1050 | "type": "object", 1051 | "properties": { 1052 | "type": { 1053 | "type": "string", 1054 | "pattern": "^PointSelector$", 1055 | "default": "PointSelector" 1056 | }, 1057 | "t": { "$ref": "#/types/duration" }, 1058 | "x": { "$ref": "#/types/dimension" }, 1059 | "y": { "$ref": "#/types/dimension" } 1060 | }, 1061 | "required": ["type"] 1062 | }, 1063 | { 1064 | "type": "object", 1065 | "properties": { 1066 | "type": { 1067 | "type": "string", 1068 | "pattern": "^FragmentSelector$", 1069 | "default": "FragmentSelector" 1070 | }, 1071 | "conformsTo": { 1072 | "type": "string", 1073 | "format": "uri", 1074 | "pattern": "^http.*$", 1075 | "default": "http://www.w3.org/TR/media-frags/" 1076 | }, 1077 | "value": { 1078 | "type:": "string" 1079 | } 1080 | }, 1081 | "required": ["type","value"] 1082 | }, 1083 | { 1084 | "type": "object", 1085 | "properties": { 1086 | "type": { 1087 | "type": "string", 1088 | "pattern": "^SvgSelector$", 1089 | "default": "SvgSelector" 1090 | }, 1091 | "value": { 1092 | "type:": "string" 1093 | } 1094 | }, 1095 | "required": ["type","value"] 1096 | }, 1097 | { 1098 | "type": "object", 1099 | "properties": { 1100 | "type": { 1101 | "type": "string", 1102 | "pattern": "^ImageApiSelector$", 1103 | "default": "ImageApiSelector" 1104 | }, 1105 | "region": { "type:": "string" }, 1106 | "size": { "type:": "string" }, 1107 | "rotation": { "type:": "string" }, 1108 | "quality": { "type:": "string" }, 1109 | "format": { "type:": "string" } 1110 | }, 1111 | "required": ["type"] 1112 | } 1113 | ] 1114 | }, 1115 | "range": { 1116 | "allOf": [ 1117 | { "$ref": "#/types/class" }, 1118 | { 1119 | "type": "object", 1120 | "properties": { 1121 | "type": { 1122 | "type": "string", 1123 | "pattern": "^Range$", 1124 | "default": "Range" 1125 | }, 1126 | "rendering": { "$ref": "#/types/external" }, 1127 | "supplementary": { 1128 | "oneOf": [ 1129 | { "$ref": "#/classes/annotationCollection" }, 1130 | { "$ref": "#/classes/annotationCollectionRef" } 1131 | ] 1132 | }, 1133 | "service": { "$ref": "#/classes/service" }, 1134 | "placeholderCanvas": { "$ref": "#/classes/placeholderCanvas" }, 1135 | "accompanyingCanvas": { "$ref": "#/classes/accompanyingCanvas" }, 1136 | "annotations": { 1137 | "type": "array", 1138 | "items": { 1139 | "oneOf": [ 1140 | { "$ref": "#/classes/annotationPage" }, 1141 | { "$ref": "#/classes/annotationPageRef"} 1142 | ] 1143 | } 1144 | }, 1145 | "thumbnail": { 1146 | "type": "array", 1147 | "items": { "$ref": "#/classes/resource" } 1148 | }, 1149 | "items": { 1150 | "type": "array", 1151 | "items": { 1152 | "oneOf": [ 1153 | { "$ref": "#/classes/specificResource" }, 1154 | { 1155 | "allOf": [ 1156 | { "$ref": "#/types/class" }, 1157 | { 1158 | "type": "object", 1159 | "properties": { 1160 | "type": { 1161 | "type": "string", 1162 | "pattern": "^Canvas$", 1163 | "default": "Canvas" 1164 | }, 1165 | "items": { 1166 | "type": "array" 1167 | } 1168 | }, 1169 | "required": ["items"] 1170 | } 1171 | ] 1172 | }, 1173 | { "$ref": "#/classes/range" }, 1174 | { "$ref": "#/classes/rangeRef" }, 1175 | { "$ref": "#/classes/canvasRef" } 1176 | ] 1177 | } 1178 | } 1179 | }, 1180 | "required":["items"] 1181 | } 1182 | ] 1183 | } 1184 | }, 1185 | "$id": "http://iiif.io/api/presentation/3/schema.json" , 1186 | "oneOf": [ 1187 | { "$ref": "#/classes/manifest" }, 1188 | { "$ref": "#/classes/collection" }, 1189 | { "$ref": "#/classes/annotationCollection" }, 1190 | { "$ref": "#/classes/annotationPage" } 1191 | ] 1192 | } 1193 | -------------------------------------------------------------------------------- /schema/schemavalidator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from jsonschema import Draft7Validator 4 | from jsonschema.exceptions import ValidationError, SchemaError, best_match, relevance 5 | import json 6 | from os import sys, path 7 | sys.path.append(path.dirname(path.dirname(path.abspath(__file__)))) 8 | from schema.error_processor import IIIFErrorParser 9 | 10 | def printPath(pathObj, fields): 11 | path = '' 12 | for item in pathObj: 13 | if isinstance(item, int): 14 | path += u'[{}]'.format(item) 15 | else: 16 | path += u'/' 17 | path += item 18 | 19 | path += '/[{}]'.format(fields) 20 | return path 21 | 22 | def validate(data, version, url): 23 | if version == '3.0': 24 | with open('schema/iiif_3_0.json') as json_file: 25 | try: 26 | schema = json.load(json_file) 27 | except ValueError as err: 28 | print ('Failed to load JSON due to: {}'.format(err)) 29 | raise 30 | 31 | try: 32 | validator = Draft7Validator(schema) 33 | results = validator.iter_errors(json.loads(data)) 34 | except SchemaError as err: 35 | print('Problem with the supplied schema:\n') 36 | print(err) 37 | raise 38 | 39 | okay = 0 40 | #print (best_match(results)) 41 | errors = sorted(results, key=relevance) 42 | #errors = [best_match(results)] 43 | error = " " 44 | errorsJson = [] 45 | if errors: 46 | print('Validation Failed') 47 | if len(errors) == 1 and 'is not valid under any of the given schemas' in errors[0].message: 48 | errors = errors[0].context 49 | 50 | 51 | # check to see if errors are relveant to IIIF asset 52 | errorParser = IIIFErrorParser(schema, json.loads(data)) 53 | relevantErrors = [] 54 | i = 0 55 | # Go through the list of errors and check to see if they are relevant 56 | # If the schema has a oneOf clause it will return errors for each oneOf 57 | # possibility. The isValid will check the type to ensure its relevant. e.g. 58 | # if a oneOf possibility is of type Collection but we have passed a Manifest 59 | # then its safe to ignore the validation error. 60 | for err in errors: 61 | if errorParser.isValid(list(err.absolute_schema_path), list(err.absolute_path)): 62 | # if it is valid we want a good error message so diagnose which oneOf is 63 | # relevant for the error we've found. 64 | if err.absolute_schema_path[-1] == 'oneOf': 65 | try: 66 | err = errorParser.diagnoseWhichOneOf(list(err.absolute_schema_path), list(err.absolute_path)) 67 | except RecursionError as error: 68 | print (error) 69 | print ('Failed to diagnose error due to recursion error. Schema: {} IIIF path: {}'.format(err.absolute_schema_path, err.absolute_path)) 70 | 71 | relevantErrors.append(err) 72 | if isinstance(err, ValidationError): 73 | relevantErrors.append(err) 74 | else: 75 | relevantErrors.extend(err) 76 | #else: 77 | # print ('Dismissing schema: {} path: {}'.format(err.absolute_schema_path, err.absolute_path)) 78 | i += 1 79 | # Remove dupes 80 | seen_titles = set() 81 | errors = [] 82 | for errorDup in relevantErrors: 83 | errorPath = errorParser.pathToJsonPath(errorDup.path) 84 | if errorPath not in seen_titles: 85 | errors.append(errorDup) 86 | seen_titles.add(errorPath) 87 | errorCount = 1 88 | # Now create some useful messsages to pass on 89 | for err in errors: 90 | detail = '' 91 | if 'title' in err.schema: 92 | detail = err.schema['title'] 93 | description = '' 94 | if 'description' in err.schema: 95 | detail += ' ' + err.schema['description'] 96 | context = err.instance 97 | if isinstance(context, dict): 98 | for key in context: 99 | if isinstance(context[key], list): 100 | context[key] = '[ ... ]' 101 | elif isinstance(context[key], dict): 102 | context[key] = '{ ... }' 103 | errorsJson.append({ 104 | 'title': 'Error {} of {}.\n Message: {}'.format(errorCount, len(errors), err.message), 105 | 'detail': detail, 106 | 'description': description, 107 | 'path': printPath(err.path, err.message), 108 | 'context': context, 109 | 'error': err 110 | 111 | }) 112 | #print (json.dumps(err.instance, indent=4)) 113 | errorCount += 1 114 | 115 | # Return: 116 | # infojson = { 117 | # 'okay': okay, 118 | # 'warnings': warnings, 119 | # 'error': str(err), 120 | # 'url': url 121 | # } 122 | 123 | okay = 0 124 | else: 125 | print ('Passed Validation!') 126 | okay = 1 127 | error = "" 128 | return { 129 | 'okay': okay, 130 | 'warnings': [], 131 | 'error': error, 132 | 'errorList': errorsJson, 133 | 'url': url 134 | } 135 | 136 | def json_path(absolute_path): 137 | path = '$' 138 | for elem in absolute_path: 139 | if isinstance(elem, int): 140 | path += '[' + str(elem) + ']' 141 | else: 142 | path += '.' + elem 143 | return path 144 | 145 | if __name__ == '__main__': 146 | if len(sys.argv) != 2: 147 | print ('Usage:\n\t{} manifest'.format(sys.argv[0])) 148 | exit(-1) 149 | with open(sys.argv[1]) as json_file: 150 | print ('Loading: {}'.format(sys.argv[1])) 151 | try: 152 | iiif_json = json.load(json_file) 153 | except ValueError as err: 154 | print ('Failed to load JSON due to: {}'.format(err)) 155 | exit(-1) 156 | 157 | result = validate(json.dumps(iiif_json), '3.0', sys.argv[1]) 158 | for error in result['errorList']: 159 | print ("Message: {}".format(error['title'])) 160 | print (" **") 161 | # print (" Validator: {}".format(error['error'].validator)) 162 | # print (" Relative Schema Path: {}".format(error['error'].relative_schema_path)) 163 | # print (" Schema Path: {}".format(error['error'].absolute_schema_path)) 164 | # print (" Relative_path: {}".format(error['error'].relative_path)) 165 | # print (" Absolute_path: {}".format(error['error'].absolute_path)) 166 | print (" Json_path: {}".format(json_path(error['error'].absolute_path))) 167 | print (" Instance: {}".format(error['error'].instance)) 168 | print (" Context: {}".format(error['error'].context)) 169 | #print (" Full: {}".format(error['error'])) 170 | 171 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """Setup for iiif/presentation-validator.""" 2 | from setuptools import setup, Command 3 | import os 4 | import re 5 | 6 | class Coverage(Command): 7 | """Class to allow coverage run from setup.""" 8 | 9 | description = "run coverage" 10 | user_options = [] 11 | 12 | def initialize_options(self): 13 | """Empty initialize_options.""" 14 | pass 15 | 16 | def finalize_options(self): 17 | """Empty finalize_options.""" 18 | pass 19 | 20 | def run(self): 21 | """Run coverage program.""" 22 | os.system("coverage run --omit=tests/*,setup.py setup.py test") 23 | os.system("coverage report") 24 | os.system("coverage html") 25 | print("See htmlcov/index.html for details.") 26 | 27 | setup( 28 | name='iiif-presentation-validator', 29 | version='0.0.3', 30 | scripts=['iiif-presentation-validator.py'], 31 | packages=['schema', 'tests'], 32 | classifiers=["Development Status :: 4 - Beta", 33 | "Intended Audience :: Developers", 34 | "Operating System :: OS Independent", 35 | "Programming Language :: Python", 36 | "Programming Language :: Python :: 3.9", 37 | "Programming Language :: Python :: 3.10", 38 | "Programming Language :: Python :: 3.11", 39 | "Topic :: Internet :: WWW/HTTP", 40 | "Topic :: Multimedia :: Graphics :: Graphics Conversion", 41 | "Topic :: Software Development :: " 42 | "Libraries :: Python Modules", 43 | "Environment :: Web Environment"], 44 | url='https://github.com/IIIF/presentation-validator', 45 | description='Validator for the IIIF Presentation API', 46 | long_description=open('README.md').read(), 47 | install_requires=[ 48 | 'bottle>=0.12.9', 49 | 'iiif_prezi>=0.2.2', 50 | 'jsonschema', 51 | 'jsonpath_rw', 52 | 'requests' 53 | ], 54 | extras_require={ 55 | ':python_version>="3.6"': ["Pillow>=3.2.0"] 56 | }, 57 | test_suite="tests", 58 | tests_require=[ 59 | "coverage", 60 | "mock", 61 | ], 62 | cmdclass={ 63 | 'coverage': Coverage, 64 | }, 65 | ) 66 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/presentation-validator/e38c1e6a5b8160327371b5419a97f9670d4266a9/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_validator.py: -------------------------------------------------------------------------------- 1 | """Test code for iiif-presentation-validator.py.""" 2 | import unittest 3 | from mock import Mock 4 | try: 5 | import imp 6 | except ImportError: 7 | import importlib 8 | 9 | from bottle import Response, request, LocalRequest 10 | 11 | try: 12 | # python3 13 | from urllib.request import URLError 14 | except ImportError: 15 | # fall back to python2 16 | from urllib2 import URLError 17 | import json 18 | from os import sys, path 19 | sys.path.append(path.dirname(path.dirname(path.abspath(__file__)))) 20 | 21 | # The validator isn't a module but with a little magic 22 | # we can load it up as if it were in order to access 23 | # the classes within 24 | fh = open('iiif-presentation-validator.py', 'r') 25 | try: 26 | val_mod = imp.load_module('ipv', fh, 'iiif-presentation-validator.py', 27 | ('py', 'r', imp.PY_SOURCE)) 28 | except: 29 | val_mod = importlib.import_module("iiif-presentation-validator") 30 | finally: 31 | fh.close() 32 | 33 | from schema.error_processor import IIIFErrorParser 34 | 35 | def read_fixture(fixture): 36 | """Read data from text fixture.""" 37 | with open(fixture, 'r') as fh: 38 | data = fh.read() 39 | return(data) 40 | 41 | 42 | class MockWSGI(object): 43 | """Mock WSGI object with data read from fixture.""" 44 | 45 | def __init__(self, fixture): 46 | """Initialize mock object with fixture filename.""" 47 | self.fixture = fixture 48 | 49 | def read(self, clen): 50 | """Read from fixture.""" 51 | return read_fixture(self.fixture) 52 | 53 | 54 | class MockWebHandle(object): 55 | """Mock WebHandle object with empty headers.""" 56 | 57 | def __init__(self): 58 | """Initialize mock object with empty headers.""" 59 | self.headers = {} 60 | 61 | 62 | class TestAll(unittest.TestCase): 63 | 64 | def test01_get_bottle_app(self): 65 | v = val_mod.Validator() 66 | self.assertTrue(v.get_bottle_app()) 67 | 68 | def test02_fetch(self): 69 | v = val_mod.Validator() 70 | (data, wh) = v.fetch('file:fixtures/1/manifest.json', 'false') 71 | self.assertTrue(data.startswith('{')) 72 | self.assertRaises(URLError, v.fetch, 'file:DOES_NOT_EXIST', 'false') 73 | 74 | def test03_check_manifest(self): 75 | v = val_mod.Validator() 76 | # good manifests 77 | for good in ('fixtures/1/manifest.json', 78 | 'fixtures/2/manifest.json'): 79 | with open(good, 'r') as fh: 80 | data = fh.read() 81 | j = json.loads(v.check_manifest(data, '2.1')) 82 | self.assertEqual(j['okay'], 1) 83 | # bad manifests 84 | for bad_data in ('', '{}'): 85 | j = json.loads(v.check_manifest(bad_data, '2.1')) 86 | self.assertEqual(j['okay'], 0) 87 | 88 | #def test04_do_POST_test(self): 89 | # """Test POST requests -- machine interaction with validator service.""" 90 | # v = val_mod.Validator() 91 | # # FIXME - nasty hack to mock data for bottle.request 92 | # m = MockWSGI('fixtures/1/manifest.json') 93 | # request.body = m 94 | # request.environ['wsgi.input'] = m.read 95 | # j = json.loads(v.do_POST_test()) 96 | # self.assertEqual(j['okay'], 1) 97 | 98 | def test05_do_GET_test(self): 99 | """Test GET requests -- typical user interaction with web form.""" 100 | # Note that attempting to set request.environ['QUERY_STRING'] to mock 101 | # input data works only the first time. Instead create a new request 102 | # object to similate each web request, with data that sets request.environ 103 | v = val_mod.Validator() 104 | request = LocalRequest({'QUERY_STRING': 'url=http://iiif.io/api/presentation/2.0/example/fixtures/1/manifest.json'}) 105 | v.fetch = Mock(return_value=(read_fixture('fixtures/1/manifest.json'), MockWebHandle())) 106 | j = json.loads(v.do_GET_test()) 107 | self.assertEqual(j['okay'], 1) 108 | self.assertEqual(j['url'], 'http://iiif.io/api/presentation/2.0/example/fixtures/1/manifest.json') 109 | # fetch failure 110 | v = val_mod.Validator() 111 | request = LocalRequest({'QUERY_STRING': 'url=http://example.org/a'}) 112 | v.fetch = Mock() 113 | v.fetch.side_effect = Exception('Fetch failed') 114 | j = json.loads(v.do_GET_test()) 115 | self.assertEqual(j['okay'], 0) 116 | self.assertTrue(j['error'].startswith('Cannot fetch url')) 117 | # bogus URL 118 | v = val_mod.Validator() 119 | request = LocalRequest({'QUERY_STRING': 'url=not_http://a.b.c/'}) 120 | v.fetch = Mock(return_value=(read_fixture('fixtures/1/manifest.json'), MockWebHandle())) 121 | j = json.loads(v.do_GET_test()) 122 | self.assertEqual(j['okay'], 0) 123 | # another bogus URL 124 | v = val_mod.Validator() 125 | request = LocalRequest({'QUERY_STRING': 'url=httpX://a.b/'}) 126 | v.fetch = Mock(return_value=(read_fixture('fixtures/1/manifest.json'), MockWebHandle())) 127 | j = json.loads(v.do_GET_test()) 128 | self.assertEqual(j['okay'], 0) 129 | # Check v3 requests pass 130 | request = LocalRequest({'QUERY_STRING': 'version=3.0&url=https://a.b/&accept=true'}) 131 | v.fetch = Mock(return_value=(read_fixture('fixtures/3/full_example.json'), MockWebHandle())) 132 | j = json.loads(v.do_GET_test()) 133 | self.assertEqual(j['okay'], 1) 134 | # Check v3 requests allow accept = false 135 | request = LocalRequest({'QUERY_STRING': 'version=3.0&url=https://a.b/&accept=false'}) 136 | v.fetch = Mock(return_value=(read_fixture('fixtures/3/full_example.json'), MockWebHandle())) 137 | j = json.loads(v.do_GET_test()) 138 | self.assertEqual(j['okay'], 1) 139 | # Check v2 requests do not validate v3 manifests 140 | request = LocalRequest({'QUERY_STRING': 'version=2.1&url=https://a.b/&accept=false'}) 141 | v.fetch = Mock(return_value=(read_fixture('fixtures/3/full_example.json'), MockWebHandle())) 142 | j = json.loads(v.do_GET_test()) 143 | self.assertEqual(j['okay'], 0) 144 | 145 | def test06_index_route(self): 146 | """Test index page.""" 147 | v = val_mod.Validator() 148 | html = v.index_route() 149 | self.assertTrue(html.startswith('')) 150 | 151 | def test07_check_manifest3(self): 152 | v = val_mod.Validator() 153 | # good manifests 154 | for good in ['fixtures/3/simple_video.json', 155 | 'fixtures/3/full_example.json', 156 | 'fixtures/3/choice.json', 157 | 'fixtures/3/collection.json', 158 | 'fixtures/3/collection_of_collections.json', 159 | 'fixtures/3/version2image.json', 160 | 'fixtures/3/annoPage.json', 161 | 'fixtures/3/anno_pointselector.json', 162 | 'fixtures/3/annoPageMultipleMotivations.json', 163 | 'fixtures/3/old_cc_license.json', 164 | 'fixtures/3/rightsstatement_license.json', 165 | 'fixtures/3/extension_anno.json', 166 | 'fixtures/3/multi_bodies.json', 167 | 'fixtures/3/publicdomain.json', 168 | 'fixtures/3/navPlace.json', 169 | 'fixtures/3/anno_source.json', 170 | 'fixtures/3/range_range.json', 171 | 'fixtures/3/accompanyingCanvas.json', 172 | 'fixtures/3/placeholderCanvas.json', 173 | 'fixtures/3/point_selector.json' 174 | ]: 175 | with open(good, 'r') as fh: 176 | print ('Testing: {}'.format(good)) 177 | data = fh.read() 178 | j = json.loads(v.check_manifest(data, '3.0')) 179 | if j['okay'] != 1: 180 | if 'errorList' in j: 181 | self.printValidationerror(good, j['errorList']) 182 | else: 183 | print ('Failed to find errors but manifest {} failed validation'.format(good)) 184 | print (j) 185 | 186 | self.assertEqual(j['okay'], 1, 'Expected manifest {} to pass validation but it failed'.format(good)) 187 | 188 | for bad_data in ['fixtures/3/broken_simple_image.json', 189 | 'fixtures/3/broken_choice.json', 190 | 'fixtures/3/broken_collection.json', 191 | 'fixtures/3/broken_embedded_annos.json', 192 | 'fixtures/3/non_cc_license.json', 193 | 'fixtures/3/collection_of_canvases.json']: 194 | with open(bad_data, 'r') as fh: 195 | data = fh.read() 196 | j = json.loads(v.check_manifest(data, '3.0')) 197 | 198 | if j['okay'] == 1: 199 | print("Expected {} to fail validation but it passed....".format(bad_data)) 200 | 201 | self.assertEqual(j['okay'], 0) 202 | 203 | def printValidationerror(self, filename, errors): 204 | print('Failed to validate: {}'.format(filename)) 205 | 206 | def test08_errortrees(self): 207 | with open('fixtures/3/broken_service.json') as json_file: 208 | iiif_json = json.load(json_file) 209 | 210 | schema_file = 'schema/iiif_3_0.json' 211 | with open(schema_file) as json_file: 212 | schema = json.load(json_file) 213 | 214 | errorParser = IIIFErrorParser(schema, iiif_json) 215 | 216 | #print (errorParser) 217 | # annotationPage 218 | path = [ u'oneOf', 2, u'allOf', 1, u'properties', u'items', u'items', u'properties', u'type', u'pattern'] 219 | iiifPath = [u'items', 0, u'type'] 220 | self.assertFalse(errorParser.isValid(path, iiifPath), 'Matched manifest to annotation page incorrectly') 221 | 222 | # annotationPage 223 | path = [u'oneOf', 2, u'allOf', 1, u'properties', u'items', u'items', u'required'] 224 | iiifPath = [u'items', 0] 225 | self.assertFalse(errorParser.isValid(path, iiifPath), 'Matched manifest to annotation page incorrectly') 226 | 227 | # annotationPage 228 | path = [u'oneOf', 2,u'allOf', 1, u'properties', u'type', u'pattern'] 229 | iiifPath = [u'type'] 230 | self.assertFalse(errorParser.isValid(path, iiifPath), 'Matched manifest to annotation page incorrectly') 231 | 232 | # annotationPage 233 | path = [u'oneOf', 2,u'allOf', 1, u'additionalProperties'] 234 | iiifPath = [] 235 | self.assertFalse(errorParser.isValid(path, iiifPath), 'Matched manifest to annotation page incorrectly') 236 | 237 | # Collection 238 | path = [u'oneOf', 1, u'allOf', 1, u'properties', u'thumbnail', u'items', u'oneOf'] 239 | iiifPath = [u'thumbnail', 0] 240 | self.assertFalse(errorParser.isValid(path, iiifPath), 'Matched manifest to collection incorrectly') 241 | 242 | # Collection 243 | path = [u'oneOf', 1, u'allOf', 1, u'properties', u'type', u'pattern'] 244 | iiifPath = [u'type'] 245 | self.assertFalse(errorParser.isValid(path, iiifPath), 'Matched manifest to collection incorrectly') 246 | 247 | # Collection 248 | path = [u'oneOf', 1, u'allOf', 1, u'properties', u'items', u'items', u'oneOf'] 249 | iiifPath = [u'items', 0] 250 | self.assertFalse(errorParser.isValid(path, iiifPath), 'Matched manifest to collection incorrectly') 251 | 252 | # annotationPage 253 | path = [u'oneOf', 0, u'allOf', 1, u'properties', u'thumbnail', u'items', u'oneOf'] 254 | iiifPath = [u'thumbnail', 0] 255 | self.assertTrue(errorParser.isValid(path, iiifPath), 'Should have caught the service in thumbnail needs to be an array.') 256 | 257 | # annotationPage 258 | path = [u'oneOf', 0, u'allOf', 1, u'properties', u'items', u'items', u'allOf', 1, u'properties', u'items', u'items', u'allOf', 1, u'properties', u'items', u'items', u'allOf', 1, u'properties', u'body', u'anyOf'] 259 | iiifPath = [u'items', 0, u'items', 0, u'items', 0, u'body'] 260 | self.assertTrue(errorParser.isValid(path, iiifPath), 'Should have caught the service in the canvas needs to be an array') 261 | 262 | with open('fixtures/3/broken_simple_image.json') as json_file: 263 | iiif_json = json.load(json_file) 264 | errorParser = IIIFErrorParser(schema, iiif_json) 265 | # Provider as list example: 266 | path = ['oneOf', 0, 'allOf', 1, 'properties', 'provider', 'items', 'allOf', 1, 'properties', 'seeAlso', 'items', 'allOf', 0, 'required'] 267 | iiifPath = ['provider', 0, 'seeAlso', 0] 268 | self.assertTrue(errorParser.isValid(path, iiifPath)) 269 | 270 | def test_version3errors(self): 271 | v = val_mod.Validator() 272 | 273 | filename = 'fixtures/3/broken_simple_image.json' 274 | errorPaths = [ 275 | '/provider[0]/logo[0]', 276 | '/provider[0]/seeAlso[0]', 277 | '/items[0]' 278 | ] 279 | response = self.helperRunValidation(v, filename) 280 | self.helperTestValidationErrors(filename, response, errorPaths) 281 | 282 | filename = 'fixtures/3/broken_service.json' 283 | errorPaths = [ 284 | '/thumbnail[0]/service', 285 | '/items[0]/items[0]/items[0]/body/' 286 | ] 287 | response = self.helperRunValidation(v, filename) 288 | self.helperTestValidationErrors(filename, response, errorPaths) 289 | 290 | filename = 'fixtures/3/old_format_label.json' 291 | errorPaths = [ 292 | '/label', 293 | '/' 294 | ] 295 | response = self.helperRunValidation(v, filename) 296 | self.helperTestValidationErrors(filename, response, errorPaths) 297 | 298 | def test_lang_rights(self): 299 | v = val_mod.Validator() 300 | 301 | filename = 'fixtures/3/rights_lang_issues.json' 302 | errorPaths = [ 303 | '/label', 304 | '/items[0]/label[0]', 305 | '/items[0]/', 306 | '/items[0]/items[0]/items[0]/body/', 307 | '/rights', 308 | '/metadata[0]/label/', 309 | '/metadata[0]/value/', 310 | '/metadata[1]/label/', 311 | '/metadata[1]/value/', 312 | '/metadata[2]/label/', 313 | '/metadata[2]/value/', 314 | '/metadata[3]/label/', 315 | '/metadata[3]/value/', 316 | '/metadata[4]/label/', 317 | '/metadata[4]/value/', 318 | '/metadata[5]/label/', 319 | '/metadata[5]/value/', 320 | '/metadata[6]/label/', 321 | '/metadata[6]/value/', 322 | '/metadata[7]/label/', 323 | '/metadata[7]/value/', 324 | '/metadata[8]/label/', 325 | '/metadata[8]/value/', 326 | '/metadata[9]/label/', 327 | '/metadata[9]/value/' 328 | ] 329 | response = self.helperRunValidation(v, filename) 330 | self.helperTestValidationErrors(filename, response, errorPaths) 331 | 332 | def formatErrors(self, errorList): 333 | response = '' 334 | for error in errorList: 335 | response += "Title: {}\n".format(error['title']) 336 | response += "Path: {}\n".format(error['path']) 337 | response += "Context: {}\n".format(error['path']) 338 | response += "****************************\n" 339 | 340 | return response 341 | 342 | def helperTestValidationErrors(self, filename, response, errorPaths): 343 | self.assertEqual(response['okay'], 0, 'Expected {} to fail validation but it past.'.format(filename)) 344 | self.assertEqual(len(response['errorList']), len(errorPaths), 'Expected {} validation errors but found {} for file {}\n{}'.format(len(errorPaths), len(response['errorList']), filename, self.formatErrors(response['errorList']))) 345 | 346 | 347 | for error in response['errorList']: 348 | foundPath = False 349 | for path in errorPaths: 350 | if error['path'].startswith(path): 351 | foundPath=True 352 | self.assertTrue(foundPath, 'Unexpected path: {} in file {}'.format(error['path'], filename)) 353 | 354 | def helperRunValidation(self, validator, iiifFile, version="3.0"): 355 | with open(iiifFile, 'r') as fh: 356 | data = fh.read() 357 | return json.loads(validator.check_manifest(data, '3.0')) 358 | 359 | errorCount = 1 360 | 361 | for err in errors: 362 | print(err['title']) 363 | print(err['detail']) 364 | print('\n Path for error: {}'.format(err['path'])) 365 | print('\n Context: {}'.format(err['context'])) 366 | errorCount += 1 367 | 368 | 369 | if __name__ == '__main__': 370 | unittest.main() 371 | --------------------------------------------------------------------------------