├── .ebextensions └── packages.config ├── .github └── workflows │ ├── PyPi.yml │ ├── TestPyPi.yml │ ├── deploy.yml │ └── test.yml ├── .gitignore ├── CHANGES.md ├── Dockerfile ├── LICENSE.txt ├── Procfile ├── README ├── README.rst ├── Vagrantfile ├── ansible ├── development-playbook.retry ├── development-playbook.yml ├── development.ini └── roles │ ├── apache │ ├── files │ │ └── iiif-validator.conf │ └── tasks │ │ └── main.yml │ ├── basic-setup │ └── tasks │ │ └── main.yml │ └── dependencies │ └── tasks │ └── main.yml ├── docker-files ├── conf.modules.d.tar.gz └── wsgi.conf ├── html ├── 67352ccc-d1b0-11e1-89ae-279075081939.jp2 ├── 67352ccc-d1b0-11e1-89ae-279075081939.png ├── css │ ├── cupertino │ │ ├── images │ │ │ ├── ui-bg_diagonals-thick_90_eeeeee_40x40.png │ │ │ ├── ui-bg_flat_15_cd0a0a_40x100.png │ │ │ ├── ui-bg_glass_100_e4f1fb_1x400.png │ │ │ ├── ui-bg_glass_50_3baae3_1x400.png │ │ │ ├── ui-bg_glass_80_d7ebf9_1x400.png │ │ │ ├── ui-bg_highlight-hard_100_f2f5f7_1x100.png │ │ │ ├── ui-bg_highlight-hard_70_000000_1x100.png │ │ │ ├── ui-bg_highlight-soft_100_deedf7_1x100.png │ │ │ ├── ui-bg_highlight-soft_25_ffef8f_1x100.png │ │ │ ├── ui-icons_2694e8_256x240.png │ │ │ ├── ui-icons_2e83ff_256x240.png │ │ │ ├── ui-icons_3d80b3_256x240.png │ │ │ ├── ui-icons_72a7cf_256x240.png │ │ │ └── ui-icons_ffffff_256x240.png │ │ └── jquery-ui-1.8.17.custom.css │ └── iiif.css ├── download.html ├── img │ └── iiif-logo-60.png ├── index.html ├── js │ ├── results.js │ ├── select.js │ └── vendor │ │ ├── jquery-1.11.1.min.js │ │ └── jquery-ui-1.8.17.custom.min.js └── results.html ├── iiif-validate.py ├── iiif-validator.py ├── iiif_validator ├── __init__.py ├── apache.py ├── tests │ ├── __init__.py │ ├── baseurl_redirect.py │ ├── cors.py │ ├── format_conneg.py │ ├── format_error_random.py │ ├── format_gif.py │ ├── format_jp2.py │ ├── format_jpg.py │ ├── format_pdf.py │ ├── format_png.py │ ├── format_tif.py │ ├── format_webp.py │ ├── id_basic.py │ ├── id_error_escapedslash.py │ ├── id_error_random.py │ ├── id_error_unescaped.py │ ├── id_escaped.py │ ├── id_squares.py │ ├── info_json.py │ ├── info_xml.py │ ├── jsonld.py │ ├── linkheader_canonical.py │ ├── linkheader_profile.py │ ├── quality_bitonal.py │ ├── quality_color.py │ ├── quality_error_random.py │ ├── quality_grey.py │ ├── region_error_random.py │ ├── region_percent.py │ ├── region_pixels.py │ ├── region_square.py │ ├── rot_error_random.py │ ├── rot_full_basic.py │ ├── rot_full_non90.py │ ├── rot_mirror.py │ ├── rot_mirror_180.py │ ├── rot_region_basic.py │ ├── rot_region_non90.py │ ├── size_bwh.py │ ├── size_ch.py │ ├── size_error_random.py │ ├── size_nofull.py │ ├── size_noup.py │ ├── size_percent.py │ ├── size_region.py │ ├── size_up.py │ ├── size_wc.py │ ├── size_wh.py │ └── test.py └── validator.py ├── implementations └── validator │ └── tests │ └── __init__.py ├── pypi_upload.md ├── requirements.txt ├── setup.py ├── tests ├── __init__.py ├── json │ ├── info-2.0.json │ ├── info-3.0-logo.json │ ├── info-3.0-service-badlabel.json │ ├── info-3.0-service-label.json │ ├── info-3.0-service.json │ └── info-3.0.json └── test_validator.py └── tox.ini /.ebextensions/packages.config: -------------------------------------------------------------------------------- 1 | packages: 2 | yum: 3 | libxml2-devel: [] 4 | libxslt-devel: [] 5 | -------------------------------------------------------------------------------- /.github/workflows/PyPi.yml: -------------------------------------------------------------------------------- 1 | name: Publish Python 🐍 distributions 📦 to PyPI on Release 2 | 3 | on: 4 | release: 5 | types: [released] 6 | 7 | jobs: 8 | build-n-publish: 9 | name: Build and publish Python 🐍 distributions 📦 to PyPI 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Set up Python 3.9 15 | uses: actions/setup-python@v5 16 | with: 17 | python-version: 3.9 18 | 19 | - name: Setup version 20 | run: echo "${{ github.event.release.tag_name }}" > version.txt 21 | 22 | - name: Install pypa/build 23 | run: python -m pip install build --user 24 | - name: Build a binary wheel and a source tarball 25 | run: python -m build --sdist --wheel --outdir dist/ 26 | 27 | - name: Publish distribution 📦 to PyPI 28 | uses: pypa/gh-action-pypi-publish@v1.12.2 29 | with: 30 | password: ${{ secrets.PYPI_API_TOKEN }} 31 | -------------------------------------------------------------------------------- /.github/workflows/TestPyPi.yml: -------------------------------------------------------------------------------- 1 | name: Publish Python 🐍 distributions 📦 to TestPyPI on Tag creation 2 | 3 | on: 4 | release: 5 | types: [prereleased] 6 | 7 | jobs: 8 | build-n-publish: 9 | name: Build and publish Python 🐍 distributions 📦 to PyPI and TestPyPI 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Set up Python 3.9 15 | uses: actions/setup-python@v5 16 | with: 17 | python-version: 3.9 18 | 19 | - name: Setup version 20 | run: echo "${{ github.event.release.tag_name }}" > version.txt 21 | 22 | - name: Install pypa/build 23 | run: python -m pip install build --user 24 | - name: Build a binary wheel and a source tarball 25 | run: python -m build --sdist --wheel --outdir dist/ 26 | 27 | 28 | - name: Publish distribution 📦 to Test PyPI 29 | uses: pypa/gh-action-pypi-publish@v1.12.2 30 | with: 31 | password: ${{ secrets.TEST_PYPI_API_TOKEN }} 32 | repository_url: https://test.pypi.org/legacy/ 33 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: deploy-to-eb 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | 7 | env: 8 | ELASTIC_BEANSTALK_NAME: iiif-website-validators 9 | ELASTIC_BEANSTALK_ENV_NAME: iiif-image-validator-python-3 10 | # Bucket where source is stored for ElasticBeanstalk 11 | BUCKET: codepipeline-us-east-1-740788099428 12 | 13 | jobs: 14 | deploy: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | # creating zip file 21 | - name: Create ZIP deployment package 22 | run: zip -r ${{github.run_id}}.zip ./ 23 | 24 | # Configuring credentials 25 | - name: Configure AWS Credentials 26 | uses: aws-actions/configure-aws-credentials@v4 27 | with: 28 | aws-access-key-id: ${{ secrets.ACCESS_KEY_ID }} 29 | aws-secret-access-key: ${{ secrets.SECRET_ACCESS_KEY }} 30 | aws-region: "us-east-1" 31 | 32 | # This bucket needs to be encrypted 33 | - name: Upload package to S3 bucket 34 | run: aws s3 cp ${{github.run_id}}.zip s3://$BUCKET/iiif-image-validator/MyApp/ --sse aws:kms 35 | 36 | - name: Create new ElasticBeanstalk Application Version 37 | run: | 38 | aws elasticbeanstalk create-application-version \ 39 | --application-name $ELASTIC_BEANSTALK_NAME \ 40 | --source-bundle S3Bucket=$BUCKET,S3Key="iiif-image-validator/MyApp/${{github.run_id}}.zip" \ 41 | --version-label "ver-${{ github.sha }}" \ 42 | --description "commit-sha-${{ github.sha }}" 43 | 44 | # deploy application 45 | - name: Deploy new ElasticBeanstalk Application Version 46 | run: aws elasticbeanstalk update-environment --environment-name $ELASTIC_BEANSTALK_ENV_NAME --version-label "ver-${{ github.sha }}" -------------------------------------------------------------------------------- /.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', '3.12'] 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 dependencies 30 | run: | 31 | python -m pip install --upgrade pip 32 | pip install -r requirements.txt 33 | pip install setuptools -U 34 | 35 | - name: Install 36 | run: python setup.py install 37 | 38 | - name: Test 39 | run: python -m unittest discover -s tests 40 | 41 | - name: install coveralls 42 | run: pip install coveralls 43 | 44 | - name: Generate coverage 45 | run: coverage run -m unittest discover -s tests 46 | 47 | - name: Upload coverage data to coveralls.io 48 | run: coveralls --service=github 49 | env: 50 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 51 | COVERALLS_FLAG_NAME: ${{ matrix.python-version }} 52 | COVERALLS_PARALLEL: true 53 | 54 | Coveralls: 55 | needs: build 56 | runs-on: ubuntu-latest 57 | container: python:3-slim 58 | steps: 59 | - name: Coveralls Finished 60 | run: | 61 | pip3 install --upgrade coveralls 62 | coveralls --service=github --finish 63 | env: 64 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.vagrant 2 | /.cache 3 | /.eggs 4 | /build 5 | /dist 6 | /iiif_validator.egg-info 7 | *.pyc 8 | *~ 9 | .tox 10 | .coverage 11 | *.swp 12 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | iiif_validator change log 2 | ========================= 3 | 4 | v1.0.5 2019-08-20 5 | * Add support for IIIF version 3.0 6 | * Removed PNG requirement for quality_bitonal test 7 | * Removed support for EOL python 2.6 8 | 9 | v1.0.4 2017-09-08 10 | * Add license information 11 | * Use Requirements-Builder and Tox for testing 12 | 13 | v1.0.3 2017-01-19 14 | * Fix CORS test 15 | 16 | v1.0.2 2017-01-19 17 | * Tie Pillow to < 4.0.0 as 4.0.0 no longer supports python 2.6 18 | 19 | v1.0.1 2015-05-20 20 | * Made to work with python 3.x as well as 2.7 21 | * Fix for Origin request header 22 | * Fix for Content-type 23 | * Timeout added on validation requests 24 | 25 | v1.0.0 2015-02-10 26 | * Has been running long enough and tested by others to declare 1.0.0 27 | * Fix issues with images with >256 colors and color palettes 28 | * Switch README to reStructuredText for pypi 29 | * Added --test param to iiif-validate.py to run specific tests 30 | 31 | v0.9.1 2014-11-11 32 | * Fix bug in validation of rotation 33 | * Update README with instructions for use in Travis CI 34 | 35 | v0.9.0 2014-11-04 36 | * Packaged for pypi and easy use with Travis CI 37 | * Used IIIF in validation service at 38 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM httpd:2.4.39 2 | 3 | RUN apt-get update 4 | RUN apt-get -y install python3.4 python3-pip libapache2-mod-wsgi-py3 libxml2-dev libxslt1-dev lib32z1-dev libjpeg-dev libmagic-dev python-dev vim 5 | 6 | RUN mkdir -p /opt/python/current/app 7 | COPY . /opt/python/current/app 8 | WORKDIR /opt/python/current/app 9 | RUN pip3 install -r requirements.txt 10 | 11 | RUN ln -s /usr/local/apache2/conf/ /etc/httpd 12 | RUN ln -s /usr/local/apache2/modules /etc/httpd/modules 13 | RUN ln -s /usr/lib/apache2/modules/mod_wsgi.so /etc/httpd/modules/mod_wsgi.so 14 | RUN mkdir /var/run/httpd 15 | RUN ln -s /var/run/httpd /etc/httpd/run 16 | RUN mkdir /var/www 17 | RUN ln -s /usr/local/apache2/htdocs /var/www/html 18 | COPY html/ /var/www/html/ 19 | RUN sed -i 's/http:\/\/iiif.io//g' /var/www/html/js/*.js 20 | RUN ln -s /usr/local/apache2/logs /var/log/httpd 21 | RUN ln -s /var/log/httpd /etc/httpd/logs 22 | RUN mkdir /etc/httpd/conf.d 23 | 24 | COPY .ebextensions/http/conf/httpd.conf /etc/httpd/httpd.conf 25 | RUN sed -i 's/User apache/User daemon/g' /etc/httpd/httpd.conf 26 | RUN sed -i 's/Group apache/Group daemon/g' /etc/httpd/httpd.conf 27 | COPY .ebextensions/http/conf.d/* /etc/httpd/conf.d 28 | COPY docker-files/wsgi.conf /etc/httpd/conf.d 29 | 30 | WORKDIR /etc/httpd/ 31 | COPY docker-files/conf.modules.d.tar.gz /tmp/ 32 | RUN tar zxvf /tmp/conf.modules.d.tar.gz 33 | 34 | RUN ln -sf /dev/stdout /var/log/httpd/access_log && ln -sf /dev/stderr /var/log/httpd/error_log 35 | 36 | #RUN rm /etc/nginx/conf.d/*.conf 37 | #COPY .ebextensions/nginx/conf.d/*.conf /etc/nginx/conf.d/ 38 | #COPY .ebextensions/nginx/conf.d/elasticbeanstalk /etc/nginx/conf.d/elasticbeanstalk 39 | EXPOSE 80 40 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: uwsgi --http :8000 --wsgi-file iiif-validator.py --master --processes 4 --threads 2 -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | ============== 2 | IIIF Validator 3 | ============== 4 | 5 | .. image:: https://github.com/github/docs/actions/workflows/test.yml/badge.svg 6 | :target: https://github.com/IIIF/image-validator/actions 7 | 8 | .. image:: https://coveralls.io/repos/github/IIIF/image-validator/badge.svg?branch=main 9 | :target: https://coveralls.io/github/IIIF/image-validator?branch=main 10 | 11 | 12 | This validator supports the same validations that are available on the 13 | `IIIF 14 | `_ website at 15 | ``_. 16 | 17 | 18 | Installation 19 | ------------ 20 | 21 | Installation from pypi 22 | ~~~~~~~~~~~~~~~~~~~~~~ 23 | 24 | The following should install scripts, library, and the necessary dependencies:: 25 | 26 | pip install iiif-validator 27 | 28 | Manual installation 29 | ~~~~~~~~~~~~~~~~~~~ 30 | 31 | Installation from `source code 32 | ` can de done with:: 33 | 34 | python setup.py install 35 | 36 | which should install scripts, library, and the necessary dependencies. Note that ``setup.py`` includes directives to pin certain libraries to particular versions for compatibility. 37 | 38 | The ``python-magic`` module requires ``libmagic`` which, on a mac, one can install with:: 39 | 40 | brew install libmagic 41 | 42 | 43 | Command line validator, ``iiif-validate.py`` 44 | -------------------------------------------- 45 | 46 | Then for an image served at ``http://localhost:8000/prefix/image_id`` 47 | the validator can be run with:: 48 | 49 | iiif-validate.py -s localhost:8000 -p prefix -i image_id --version=2.0 -v 50 | 51 | or similar to validate server with the test image. Use 52 | ``iiif-validate -h`` for parameter details. 53 | 54 | 55 | Use with Travis CI 56 | ~~~~~~~~~~~~~~~~~~ 57 | 58 | To install dependencies for this code the following lines must 59 | be present in the ``install:`` section of ``.travis.yml``:: 60 | 61 | install: 62 | - sudo apt-get update 63 | - sudo apt-get install libmagic-dev 64 | - pip install iiif_validator 65 | ... 66 | 67 | and then a single validation can be added to the commands under 68 | the ``script:`` section of ``.travis.yml``. For example, to test a 69 | server running with base URI ``http://localhost:8000/prefix`` with 70 | image ``image_id1`` at version 1.1, level 1, one might use:: 71 | 72 | script: 73 | ... 74 | - iiif-validate.py -s localhost:8000 -p prefix -i image_id1 --version=1.1 --level 1 --quiet 75 | 76 | The ``iiif-validate.py`` script returns 0 exit code on success, non-zero 77 | on failure, in order to work easily with Travis CI. 78 | 79 | 80 | Running the validator server, ``iiif-validator.py`` 81 | --------------------------------------------------- 82 | 83 | The primary installation of the validator server is on the `IIIF 84 | `_ website at 85 | ``_. 86 | 87 | Stand-alone server 88 | ~~~~~~~~~~~~~~~~~~ 89 | 90 | The validator server runs at ``_ by 91 | default, the URI path is the test name and then the query 92 | parameters give the details of the server and image to 93 | be tested. The URI pattern is:: 94 | 95 | http://localhost:8080/{test_name}?server={server}&prefix={prefix}&identifer={id}&version={api_version} 96 | 97 | As an example, if the test server is installed and run locally:: 98 | 99 | easy_install iiif 100 | iiif_testserver.py 101 | 102 | which will set up a server at ``_ and write the log of accesses to STDOUT. The one can run validation tests against this with requests like ``_ which tests the `info.json` response and gives JSON output (pretty formatted here):: 103 | 104 | { 105 | "test": "info_json", 106 | "status": "success", 107 | "tests": ["required-field: width", "required-field: height", "type-is-int: height", "type-is-int: width", "required-field: @id", "type-is-uri: @id", "@id is correct URI", "required-field: @context", "correct-context", "required-field: protocol", "correct-protocol", "required-field: profile", "is-list", "profile-compliance", "is-list", "is-object", "required-field: scaleFactors", "required-field: width", "type-is-int: width"], 108 | "url": ["http://localhost:8000/2.0_pil_none/67352ccc-d1b0-11e1-89ae-279075081939.png/info.json"], 109 | "label": "Check Image Information" 110 | } 111 | 112 | and ``_ which tests mirroring and will give JSON output (again pretty formatted):: 113 | 114 | { 115 | "test": "rot_mirror", 116 | "status": "success", 117 | "tests": ["9,0:True", "0,9:True"], 118 | "url": ["http://localhost:8000/2.0_pil_none/67352ccc-d1b0-11e1-89ae-279075081939.png/full/full/!0/default.jpg"], 119 | "label": "Mirroring" 120 | } 121 | 122 | 123 | Validator server under WSGI 124 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 125 | 126 | *need docs here* 127 | 128 | Running with Vagrant 129 | -------------------- 130 | 131 | For ease of development and local testing, you can provision a virtual machine using Vagrant. To start Vagrant, ssh in, and run the validator on a site:: 132 | 133 | vagrant up 134 | vagrant ssh 135 | cd /vagrant 136 | ./iiif-validate.py -h 137 | ./iiif-validate.py --scheme=https -s iiif.lib.ncsu.edu -p iiif --level=2 -i 67352ccc-d1b0-11e1-89ae-279075081939 138 | 139 | In addition, the Vagrant configuration runs the validator as a WSGI application using ``mod_wsgi`` and Apache ``httpd``, accessible both from with the VM as well as the host machine at ``_. 140 | 141 | Running with Docker 142 | -------------------- 143 | 144 | ``` 145 | docker build -t image-validator . && docker run --rm --name image-validator -p 9001:80 image-validator:latest 146 | ``` 147 | 148 | Then navigate to: 149 | 150 | http://localhost:9001/api/image/validator/service/id_basic?version=2.0&server=server&prefix=prefix&identifier=identifier 151 | 152 | 153 | Editing this README 154 | ------------------- 155 | 156 | This README is in reStructuredText and not gfm because that is what pypi will render. An editor like ``_ is useful for editing/checking interactively. 157 | 158 | License 159 | ------- 160 | 161 | Licensed under the Apache License, Version 2.0 (the "License"); 162 | you may not use this file except in compliance with the License. 163 | You may obtain a copy of the License at 164 | 165 | http://www.apache.org/licenses/LICENSE-2.0 166 | 167 | Unless required by applicable law or agreed to in writing, software 168 | distributed under the License is distributed on an "AS IS" BASIS, 169 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 170 | See the License for the specific language governing permissions and 171 | limitations under the License. 172 | 173 | See ``LICENSE.txt``. 174 | 175 | Data included with this software, such as the test image files in the ``html`` directory, may be freely reused under `CC0 176 | `_ 177 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | README -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | # All Vagrant configuration is done below. The "2" in Vagrant.configure 5 | # configures the configuration version (we support older styles for 6 | # backwards compatibility). Please don't change it unless you know what 7 | # you're doing. 8 | Vagrant.configure("2") do |config| 9 | # The most common configuration options are documented and commented below. 10 | # For a complete reference, please see the online documentation at 11 | # https://docs.vagrantup.com. 12 | 13 | # Every Vagrant development environment requires a box. You can search for 14 | # boxes at https://atlas.hashicorp.com/search. 15 | config.vm.box = "puphpet/centos65-x64" 16 | 17 | config.vm.network "private_network", ip: "192.168.33.39" 18 | 19 | # default forwarded port for IIIF validator served through Apache + mod_wsgi 20 | config.vm.network "forwarded_port", guest: "8080", host: "8080", auto_correct: true 21 | 22 | config.vm.synced_folder '.', '/vagrant' 23 | 24 | config.vm.provider "virtualbox" do |vb| 25 | vb.linked_clone = true 26 | vb.memory = 1024 27 | vb.cpus = 1 28 | end 29 | 30 | config.vm.provision "ansible_local" do |ansible| 31 | ansible.playbook = 'ansible/development-playbook.yml' 32 | ansible.inventory_path = 'ansible/development.ini' 33 | ansible.limit = 'all' 34 | # ansible.verbose = 'vvvv' 35 | end 36 | 37 | 38 | # Disable automatic box update checking. If you disable this, then 39 | # boxes will only be checked for updates when the user runs 40 | # `vagrant box outdated`. This is not recommended. 41 | # config.vm.box_check_update = false 42 | 43 | # Create a forwarded port mapping which allows access to a specific port 44 | # within the machine from a port on the host machine. In the example below, 45 | # accessing "localhost:8080" will access port 80 on the guest machine. 46 | # config.vm.network "forwarded_port", guest: 80, host: 8080 47 | 48 | # Create a private network, which allows host-only access to the machine 49 | # using a specific IP. 50 | # config.vm.network "private_network", ip: "192.168.33.10" 51 | 52 | # Create a public network, which generally matched to bridged network. 53 | # Bridged networks make the machine appear as another physical device on 54 | # your network. 55 | # config.vm.network "public_network" 56 | 57 | # Share an additional folder to the guest VM. The first argument is 58 | # the path on the host to the actual folder. The second argument is 59 | # the path on the guest to mount the folder. And the optional third 60 | # argument is a set of non-required options. 61 | # config.vm.synced_folder "../data", "/vagrant_data" 62 | 63 | # Provider-specific configuration so you can fine-tune various 64 | # backing providers for Vagrant. These expose provider-specific options. 65 | # Example for VirtualBox: 66 | # 67 | # config.vm.provider "virtualbox" do |vb| 68 | # # Display the VirtualBox GUI when booting the machine 69 | # vb.gui = true 70 | # 71 | # # Customize the amount of memory on the VM: 72 | # vb.memory = "1024" 73 | # end 74 | # 75 | # View the documentation for the provider you are using for more 76 | # information on available options. 77 | 78 | # Define a Vagrant Push strategy for pushing to Atlas. Other push strategies 79 | # such as FTP and Heroku are also available. See the documentation at 80 | # https://docs.vagrantup.com/v2/push/atlas.html for more information. 81 | # config.push.define "atlas" do |push| 82 | # push.app = "YOUR_ATLAS_USERNAME/YOUR_APPLICATION_NAME" 83 | # end 84 | 85 | # Enable provisioning with a shell script. Additional provisioners such as 86 | # Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the 87 | # documentation for more information about their specific syntax and use. 88 | # config.vm.provision "shell", inline: <<-SHELL 89 | # apt-get update 90 | # apt-get install -y apache2 91 | # SHELL 92 | end 93 | -------------------------------------------------------------------------------- /ansible/development-playbook.retry: -------------------------------------------------------------------------------- 1 | localhost 2 | -------------------------------------------------------------------------------- /ansible/development-playbook.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: image-validator 3 | become: yes 4 | become_method: sudo 5 | become_user: root 6 | remote_user: root 7 | gather_facts: yes 8 | roles: 9 | - role: basic-setup 10 | - role: dependencies 11 | - role: apache 12 | -------------------------------------------------------------------------------- /ansible/development.ini: -------------------------------------------------------------------------------- 1 | [image-validator] 2 | localhost ansible_connection=local 3 | -------------------------------------------------------------------------------- /ansible/roles/apache/files/iiif-validator.conf: -------------------------------------------------------------------------------- 1 | Listen 8080 2 | WSGISocketPrefix /var/run/wsgi 3 | WSGIDaemonProcess image-validator user=www-data group=www-data processes=1 threads=5 python-path=/vagrant 4 | WSGIScriptAlias / /vagrant/iiif-validator.py 5 | WSGIProcessGroup image-validator 6 | 7 | Order deny,allow 8 | Allow from all 9 | 10 | -------------------------------------------------------------------------------- /ansible/roles/apache/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: install Apache and mod_wsgi 3 | yum: 4 | pkg: "{{item}}" 5 | state: present 6 | update_cache: yes 7 | with_items: 8 | - httpd 9 | - mod_wsgi 10 | - libselinux-python 11 | - name: Copy and symlink validator Apache config 12 | copy: 13 | src: iiif-validator.conf 14 | dest: /etc/httpd/conf.d/zzz-iiif-validator.conf 15 | mode: 0644 16 | owner: root 17 | group: root 18 | - name: Start Apache HTTPD and enable service on boot 19 | service: 20 | name: httpd 21 | state: restarted 22 | enabled: yes -------------------------------------------------------------------------------- /ansible/roles/basic-setup/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: install epel-release 3 | yum: 4 | name: epel-release 5 | state: present 6 | - name: install useful packages 7 | yum: 8 | pkg: "{{item}}" 9 | state: present 10 | update_cache: yes 11 | with_items: 12 | - git-core 13 | - curl 14 | - vim 15 | - nano 16 | - tree 17 | - htop 18 | -------------------------------------------------------------------------------- /ansible/roles/dependencies/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: install dependencies 3 | yum: 4 | pkg: "{{item}}" 5 | state: present 6 | update_cache: yes 7 | with_items: 8 | - python-devel 9 | - python-pip 10 | - libxml2-devel 11 | - ImageMagick-devel 12 | - libwebp-devel 13 | - python-lxml 14 | - python-bottle 15 | - python-magic 16 | - name: install Pillow 17 | pip: 18 | name: "{{item}}" 19 | with_items: 20 | - Pillow 21 | -------------------------------------------------------------------------------- /docker-files/conf.modules.d.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/image-validator/6d7f3e16871aefa451ed398d90743e3c218fe5be/docker-files/conf.modules.d.tar.gz -------------------------------------------------------------------------------- /docker-files/wsgi.conf: -------------------------------------------------------------------------------- 1 | LoadModule wsgi_module modules/mod_wsgi.so 2 | WSGISocketPrefix run/wsgi 3 | WSGIRestrictEmbedded On 4 | 5 | 6 | 7 | Alias /static/ /opt/python/current/app/static/ 8 | 9 | Order allow,deny 10 | Allow from all 11 | 12 | 13 | 14 | WSGIScriptAlias /api/image/validator/service /opt/python/current/app/iiif-validator.py 15 | 16 | 17 | 18 | Require all granted 19 | 20 | 21 | WSGIDaemonProcess wsgi processes=1 threads=15 display-name=%{GROUP} \ 22 | python-path=/opt/python/current/app user=daemon group=daemon \ 23 | home=/opt/python/current/app 24 | WSGIProcessGroup wsgi 25 | 26 | 27 | LogFormat "%h (%{X-Forwarded-For}i) %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined 28 | -------------------------------------------------------------------------------- /html/67352ccc-d1b0-11e1-89ae-279075081939.jp2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/image-validator/6d7f3e16871aefa451ed398d90743e3c218fe5be/html/67352ccc-d1b0-11e1-89ae-279075081939.jp2 -------------------------------------------------------------------------------- /html/67352ccc-d1b0-11e1-89ae-279075081939.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/image-validator/6d7f3e16871aefa451ed398d90743e3c218fe5be/html/67352ccc-d1b0-11e1-89ae-279075081939.png -------------------------------------------------------------------------------- /html/css/cupertino/images/ui-bg_diagonals-thick_90_eeeeee_40x40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/image-validator/6d7f3e16871aefa451ed398d90743e3c218fe5be/html/css/cupertino/images/ui-bg_diagonals-thick_90_eeeeee_40x40.png -------------------------------------------------------------------------------- /html/css/cupertino/images/ui-bg_flat_15_cd0a0a_40x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/image-validator/6d7f3e16871aefa451ed398d90743e3c218fe5be/html/css/cupertino/images/ui-bg_flat_15_cd0a0a_40x100.png -------------------------------------------------------------------------------- /html/css/cupertino/images/ui-bg_glass_100_e4f1fb_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/image-validator/6d7f3e16871aefa451ed398d90743e3c218fe5be/html/css/cupertino/images/ui-bg_glass_100_e4f1fb_1x400.png -------------------------------------------------------------------------------- /html/css/cupertino/images/ui-bg_glass_50_3baae3_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/image-validator/6d7f3e16871aefa451ed398d90743e3c218fe5be/html/css/cupertino/images/ui-bg_glass_50_3baae3_1x400.png -------------------------------------------------------------------------------- /html/css/cupertino/images/ui-bg_glass_80_d7ebf9_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/image-validator/6d7f3e16871aefa451ed398d90743e3c218fe5be/html/css/cupertino/images/ui-bg_glass_80_d7ebf9_1x400.png -------------------------------------------------------------------------------- /html/css/cupertino/images/ui-bg_highlight-hard_100_f2f5f7_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/image-validator/6d7f3e16871aefa451ed398d90743e3c218fe5be/html/css/cupertino/images/ui-bg_highlight-hard_100_f2f5f7_1x100.png -------------------------------------------------------------------------------- /html/css/cupertino/images/ui-bg_highlight-hard_70_000000_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/image-validator/6d7f3e16871aefa451ed398d90743e3c218fe5be/html/css/cupertino/images/ui-bg_highlight-hard_70_000000_1x100.png -------------------------------------------------------------------------------- /html/css/cupertino/images/ui-bg_highlight-soft_100_deedf7_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/image-validator/6d7f3e16871aefa451ed398d90743e3c218fe5be/html/css/cupertino/images/ui-bg_highlight-soft_100_deedf7_1x100.png -------------------------------------------------------------------------------- /html/css/cupertino/images/ui-bg_highlight-soft_25_ffef8f_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/image-validator/6d7f3e16871aefa451ed398d90743e3c218fe5be/html/css/cupertino/images/ui-bg_highlight-soft_25_ffef8f_1x100.png -------------------------------------------------------------------------------- /html/css/cupertino/images/ui-icons_2694e8_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/image-validator/6d7f3e16871aefa451ed398d90743e3c218fe5be/html/css/cupertino/images/ui-icons_2694e8_256x240.png -------------------------------------------------------------------------------- /html/css/cupertino/images/ui-icons_2e83ff_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/image-validator/6d7f3e16871aefa451ed398d90743e3c218fe5be/html/css/cupertino/images/ui-icons_2e83ff_256x240.png -------------------------------------------------------------------------------- /html/css/cupertino/images/ui-icons_3d80b3_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/image-validator/6d7f3e16871aefa451ed398d90743e3c218fe5be/html/css/cupertino/images/ui-icons_3d80b3_256x240.png -------------------------------------------------------------------------------- /html/css/cupertino/images/ui-icons_72a7cf_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/image-validator/6d7f3e16871aefa451ed398d90743e3c218fe5be/html/css/cupertino/images/ui-icons_72a7cf_256x240.png -------------------------------------------------------------------------------- /html/css/cupertino/images/ui-icons_ffffff_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/image-validator/6d7f3e16871aefa451ed398d90743e3c218fe5be/html/css/cupertino/images/ui-icons_ffffff_256x240.png -------------------------------------------------------------------------------- /html/css/cupertino/jquery-ui-1.8.17.custom.css: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery UI CSS Framework 1.8.17 3 | * 4 | * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) 5 | * Dual licensed under the MIT or GPL Version 2 licenses. 6 | * http://jquery.org/license 7 | * 8 | * http://docs.jquery.com/UI/Theming/API 9 | */ 10 | 11 | /* Layout helpers 12 | ----------------------------------*/ 13 | .ui-helper-hidden { display: none; } 14 | .ui-helper-hidden-accessible { position: absolute !important; clip: rect(1px 1px 1px 1px); clip: rect(1px,1px,1px,1px); } 15 | .ui-helper-reset { margin: 0; padding: 0; border: 0; outline: 0; line-height: 1.3; text-decoration: none; font-size: 100%; list-style: none; } 16 | .ui-helper-clearfix:before, .ui-helper-clearfix:after { content: ""; display: table; } 17 | .ui-helper-clearfix:after { clear: both; } 18 | .ui-helper-clearfix { zoom: 1; } 19 | .ui-helper-zfix { width: 100%; height: 100%; top: 0; left: 0; position: absolute; opacity: 0; filter:Alpha(Opacity=0); } 20 | 21 | 22 | /* Interaction Cues 23 | ----------------------------------*/ 24 | .ui-state-disabled { cursor: default !important; } 25 | 26 | 27 | /* Icons 28 | ----------------------------------*/ 29 | 30 | /* states and images */ 31 | .ui-icon { display: block; text-indent: -99999px; overflow: hidden; background-repeat: no-repeat; } 32 | 33 | 34 | /* Misc visuals 35 | ----------------------------------*/ 36 | 37 | /* Overlays */ 38 | .ui-widget-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; } 39 | 40 | 41 | /* 42 | * jQuery UI CSS Framework 1.8.17 43 | * 44 | * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) 45 | * Dual licensed under the MIT or GPL Version 2 licenses. 46 | * http://jquery.org/license 47 | * 48 | * http://docs.jquery.com/UI/Theming/API 49 | * 50 | * To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Lucida%20Grande,%20Lucida%20Sans,%20Arial,%20sans-serif&fwDefault=bold&fsDefault=1.1em&cornerRadius=6px&bgColorHeader=deedf7&bgTextureHeader=03_highlight_soft.png&bgImgOpacityHeader=100&borderColorHeader=aed0ea&fcHeader=222222&iconColorHeader=72a7cf&bgColorContent=f2f5f7&bgTextureContent=04_highlight_hard.png&bgImgOpacityContent=100&borderColorContent=dddddd&fcContent=362b36&iconColorContent=72a7cf&bgColorDefault=d7ebf9&bgTextureDefault=02_glass.png&bgImgOpacityDefault=80&borderColorDefault=aed0ea&fcDefault=2779aa&iconColorDefault=3d80b3&bgColorHover=e4f1fb&bgTextureHover=02_glass.png&bgImgOpacityHover=100&borderColorHover=74b2e2&fcHover=0070a3&iconColorHover=2694e8&bgColorActive=3baae3&bgTextureActive=02_glass.png&bgImgOpacityActive=50&borderColorActive=2694e8&fcActive=ffffff&iconColorActive=ffffff&bgColorHighlight=ffef8f&bgTextureHighlight=03_highlight_soft.png&bgImgOpacityHighlight=25&borderColorHighlight=f9dd34&fcHighlight=363636&iconColorHighlight=2e83ff&bgColorError=cd0a0a&bgTextureError=01_flat.png&bgImgOpacityError=15&borderColorError=cd0a0a&fcError=ffffff&iconColorError=ffffff&bgColorOverlay=eeeeee&bgTextureOverlay=08_diagonals_thick.png&bgImgOpacityOverlay=90&opacityOverlay=80&bgColorShadow=000000&bgTextureShadow=04_highlight_hard.png&bgImgOpacityShadow=70&opacityShadow=30&thicknessShadow=7px&offsetTopShadow=-7px&offsetLeftShadow=-7px&cornerRadiusShadow=8px 51 | */ 52 | 53 | 54 | /* Component containers 55 | ----------------------------------*/ 56 | .ui-widget { font-family: Lucida Grande, Lucida Sans, Arial, sans-serif; font-size: 1.1em; } 57 | .ui-widget .ui-widget { font-size: 1em; } 58 | .ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button { font-family: Lucida Grande, Lucida Sans, Arial, sans-serif; font-size: 1em; } 59 | .ui-widget-content { border: 1px solid #dddddd; background: #f2f5f7 url(images/ui-bg_highlight-hard_100_f2f5f7_1x100.png) 50% top repeat-x; color: #362b36; } 60 | .ui-widget-content a { color: #362b36; } 61 | .ui-widget-header { border: 1px solid #aed0ea; background: #deedf7 url(images/ui-bg_highlight-soft_100_deedf7_1x100.png) 50% 50% repeat-x; color: #222222; font-weight: bold; } 62 | .ui-widget-header a { color: #222222; } 63 | 64 | /* Interaction states 65 | ----------------------------------*/ 66 | .ui-state-default, .ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default { border: 1px solid #aed0ea; background: #d7ebf9 url(images/ui-bg_glass_80_d7ebf9_1x400.png) 50% 50% repeat-x; font-weight: bold; color: #2779aa; } 67 | .ui-state-default a, .ui-state-default a:link, .ui-state-default a:visited { color: #2779aa; text-decoration: none; } 68 | .ui-state-hover, .ui-widget-content .ui-state-hover, .ui-widget-header .ui-state-hover, .ui-state-focus, .ui-widget-content .ui-state-focus, .ui-widget-header .ui-state-focus { border: 1px solid #74b2e2; background: #e4f1fb url(images/ui-bg_glass_100_e4f1fb_1x400.png) 50% 50% repeat-x; font-weight: bold; color: #0070a3; } 69 | .ui-state-hover a, .ui-state-hover a:hover { color: #0070a3; text-decoration: none; } 70 | .ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active { border: 1px solid #2694e8; background: #3baae3 url(images/ui-bg_glass_50_3baae3_1x400.png) 50% 50% repeat-x; font-weight: bold; color: #ffffff; } 71 | .ui-state-active a, .ui-state-active a:link, .ui-state-active a:visited { color: #ffffff; text-decoration: none; } 72 | .ui-widget :active { outline: none; } 73 | 74 | /* Interaction Cues 75 | ----------------------------------*/ 76 | .ui-state-highlight, .ui-widget-content .ui-state-highlight, .ui-widget-header .ui-state-highlight {border: 1px solid #f9dd34; background: #ffef8f url(images/ui-bg_highlight-soft_25_ffef8f_1x100.png) 50% top repeat-x; color: #363636; } 77 | .ui-state-highlight a, .ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a { color: #363636; } 78 | .ui-state-error, .ui-widget-content .ui-state-error, .ui-widget-header .ui-state-error {border: 1px solid #cd0a0a; background: #cd0a0a url(images/ui-bg_flat_15_cd0a0a_40x100.png) 50% 50% repeat-x; color: #ffffff; } 79 | .ui-state-error a, .ui-widget-content .ui-state-error a, .ui-widget-header .ui-state-error a { color: #ffffff; } 80 | .ui-state-error-text, .ui-widget-content .ui-state-error-text, .ui-widget-header .ui-state-error-text { color: #ffffff; } 81 | .ui-priority-primary, .ui-widget-content .ui-priority-primary, .ui-widget-header .ui-priority-primary { font-weight: bold; } 82 | .ui-priority-secondary, .ui-widget-content .ui-priority-secondary, .ui-widget-header .ui-priority-secondary { opacity: .7; filter:Alpha(Opacity=70); font-weight: normal; } 83 | .ui-state-disabled, .ui-widget-content .ui-state-disabled, .ui-widget-header .ui-state-disabled { opacity: .35; filter:Alpha(Opacity=35); background-image: none; } 84 | 85 | /* Icons 86 | ----------------------------------*/ 87 | 88 | /* states and images */ 89 | .ui-icon { width: 16px; height: 16px; background-image: url(images/ui-icons_72a7cf_256x240.png); } 90 | .ui-widget-content .ui-icon {background-image: url(images/ui-icons_72a7cf_256x240.png); } 91 | .ui-widget-header .ui-icon {background-image: url(images/ui-icons_72a7cf_256x240.png); } 92 | .ui-state-default .ui-icon { background-image: url(images/ui-icons_3d80b3_256x240.png); } 93 | .ui-state-hover .ui-icon, .ui-state-focus .ui-icon {background-image: url(images/ui-icons_2694e8_256x240.png); } 94 | .ui-state-active .ui-icon {background-image: url(images/ui-icons_ffffff_256x240.png); } 95 | .ui-state-highlight .ui-icon {background-image: url(images/ui-icons_2e83ff_256x240.png); } 96 | .ui-state-error .ui-icon, .ui-state-error-text .ui-icon {background-image: url(images/ui-icons_ffffff_256x240.png); } 97 | 98 | /* positioning */ 99 | .ui-icon-carat-1-n { background-position: 0 0; } 100 | .ui-icon-carat-1-ne { background-position: -16px 0; } 101 | .ui-icon-carat-1-e { background-position: -32px 0; } 102 | .ui-icon-carat-1-se { background-position: -48px 0; } 103 | .ui-icon-carat-1-s { background-position: -64px 0; } 104 | .ui-icon-carat-1-sw { background-position: -80px 0; } 105 | .ui-icon-carat-1-w { background-position: -96px 0; } 106 | .ui-icon-carat-1-nw { background-position: -112px 0; } 107 | .ui-icon-carat-2-n-s { background-position: -128px 0; } 108 | .ui-icon-carat-2-e-w { background-position: -144px 0; } 109 | .ui-icon-triangle-1-n { background-position: 0 -16px; } 110 | .ui-icon-triangle-1-ne { background-position: -16px -16px; } 111 | .ui-icon-triangle-1-e { background-position: -32px -16px; } 112 | .ui-icon-triangle-1-se { background-position: -48px -16px; } 113 | .ui-icon-triangle-1-s { background-position: -64px -16px; } 114 | .ui-icon-triangle-1-sw { background-position: -80px -16px; } 115 | .ui-icon-triangle-1-w { background-position: -96px -16px; } 116 | .ui-icon-triangle-1-nw { background-position: -112px -16px; } 117 | .ui-icon-triangle-2-n-s { background-position: -128px -16px; } 118 | .ui-icon-triangle-2-e-w { background-position: -144px -16px; } 119 | .ui-icon-arrow-1-n { background-position: 0 -32px; } 120 | .ui-icon-arrow-1-ne { background-position: -16px -32px; } 121 | .ui-icon-arrow-1-e { background-position: -32px -32px; } 122 | .ui-icon-arrow-1-se { background-position: -48px -32px; } 123 | .ui-icon-arrow-1-s { background-position: -64px -32px; } 124 | .ui-icon-arrow-1-sw { background-position: -80px -32px; } 125 | .ui-icon-arrow-1-w { background-position: -96px -32px; } 126 | .ui-icon-arrow-1-nw { background-position: -112px -32px; } 127 | .ui-icon-arrow-2-n-s { background-position: -128px -32px; } 128 | .ui-icon-arrow-2-ne-sw { background-position: -144px -32px; } 129 | .ui-icon-arrow-2-e-w { background-position: -160px -32px; } 130 | .ui-icon-arrow-2-se-nw { background-position: -176px -32px; } 131 | .ui-icon-arrowstop-1-n { background-position: -192px -32px; } 132 | .ui-icon-arrowstop-1-e { background-position: -208px -32px; } 133 | .ui-icon-arrowstop-1-s { background-position: -224px -32px; } 134 | .ui-icon-arrowstop-1-w { background-position: -240px -32px; } 135 | .ui-icon-arrowthick-1-n { background-position: 0 -48px; } 136 | .ui-icon-arrowthick-1-ne { background-position: -16px -48px; } 137 | .ui-icon-arrowthick-1-e { background-position: -32px -48px; } 138 | .ui-icon-arrowthick-1-se { background-position: -48px -48px; } 139 | .ui-icon-arrowthick-1-s { background-position: -64px -48px; } 140 | .ui-icon-arrowthick-1-sw { background-position: -80px -48px; } 141 | .ui-icon-arrowthick-1-w { background-position: -96px -48px; } 142 | .ui-icon-arrowthick-1-nw { background-position: -112px -48px; } 143 | .ui-icon-arrowthick-2-n-s { background-position: -128px -48px; } 144 | .ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; } 145 | .ui-icon-arrowthick-2-e-w { background-position: -160px -48px; } 146 | .ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; } 147 | .ui-icon-arrowthickstop-1-n { background-position: -192px -48px; } 148 | .ui-icon-arrowthickstop-1-e { background-position: -208px -48px; } 149 | .ui-icon-arrowthickstop-1-s { background-position: -224px -48px; } 150 | .ui-icon-arrowthickstop-1-w { background-position: -240px -48px; } 151 | .ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; } 152 | .ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; } 153 | .ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; } 154 | .ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; } 155 | .ui-icon-arrowreturn-1-w { background-position: -64px -64px; } 156 | .ui-icon-arrowreturn-1-n { background-position: -80px -64px; } 157 | .ui-icon-arrowreturn-1-e { background-position: -96px -64px; } 158 | .ui-icon-arrowreturn-1-s { background-position: -112px -64px; } 159 | .ui-icon-arrowrefresh-1-w { background-position: -128px -64px; } 160 | .ui-icon-arrowrefresh-1-n { background-position: -144px -64px; } 161 | .ui-icon-arrowrefresh-1-e { background-position: -160px -64px; } 162 | .ui-icon-arrowrefresh-1-s { background-position: -176px -64px; } 163 | .ui-icon-arrow-4 { background-position: 0 -80px; } 164 | .ui-icon-arrow-4-diag { background-position: -16px -80px; } 165 | .ui-icon-extlink { background-position: -32px -80px; } 166 | .ui-icon-newwin { background-position: -48px -80px; } 167 | .ui-icon-refresh { background-position: -64px -80px; } 168 | .ui-icon-shuffle { background-position: -80px -80px; } 169 | .ui-icon-transfer-e-w { background-position: -96px -80px; } 170 | .ui-icon-transferthick-e-w { background-position: -112px -80px; } 171 | .ui-icon-folder-collapsed { background-position: 0 -96px; } 172 | .ui-icon-folder-open { background-position: -16px -96px; } 173 | .ui-icon-document { background-position: -32px -96px; } 174 | .ui-icon-document-b { background-position: -48px -96px; } 175 | .ui-icon-note { background-position: -64px -96px; } 176 | .ui-icon-mail-closed { background-position: -80px -96px; } 177 | .ui-icon-mail-open { background-position: -96px -96px; } 178 | .ui-icon-suitcase { background-position: -112px -96px; } 179 | .ui-icon-comment { background-position: -128px -96px; } 180 | .ui-icon-person { background-position: -144px -96px; } 181 | .ui-icon-print { background-position: -160px -96px; } 182 | .ui-icon-trash { background-position: -176px -96px; } 183 | .ui-icon-locked { background-position: -192px -96px; } 184 | .ui-icon-unlocked { background-position: -208px -96px; } 185 | .ui-icon-bookmark { background-position: -224px -96px; } 186 | .ui-icon-tag { background-position: -240px -96px; } 187 | .ui-icon-home { background-position: 0 -112px; } 188 | .ui-icon-flag { background-position: -16px -112px; } 189 | .ui-icon-calendar { background-position: -32px -112px; } 190 | .ui-icon-cart { background-position: -48px -112px; } 191 | .ui-icon-pencil { background-position: -64px -112px; } 192 | .ui-icon-clock { background-position: -80px -112px; } 193 | .ui-icon-disk { background-position: -96px -112px; } 194 | .ui-icon-calculator { background-position: -112px -112px; } 195 | .ui-icon-zoomin { background-position: -128px -112px; } 196 | .ui-icon-zoomout { background-position: -144px -112px; } 197 | .ui-icon-search { background-position: -160px -112px; } 198 | .ui-icon-wrench { background-position: -176px -112px; } 199 | .ui-icon-gear { background-position: -192px -112px; } 200 | .ui-icon-heart { background-position: -208px -112px; } 201 | .ui-icon-star { background-position: -224px -112px; } 202 | .ui-icon-link { background-position: -240px -112px; } 203 | .ui-icon-cancel { background-position: 0 -128px; } 204 | .ui-icon-plus { background-position: -16px -128px; } 205 | .ui-icon-plusthick { background-position: -32px -128px; } 206 | .ui-icon-minus { background-position: -48px -128px; } 207 | .ui-icon-minusthick { background-position: -64px -128px; } 208 | .ui-icon-close { background-position: -80px -128px; } 209 | .ui-icon-closethick { background-position: -96px -128px; } 210 | .ui-icon-key { background-position: -112px -128px; } 211 | .ui-icon-lightbulb { background-position: -128px -128px; } 212 | .ui-icon-scissors { background-position: -144px -128px; } 213 | .ui-icon-clipboard { background-position: -160px -128px; } 214 | .ui-icon-copy { background-position: -176px -128px; } 215 | .ui-icon-contact { background-position: -192px -128px; } 216 | .ui-icon-image { background-position: -208px -128px; } 217 | .ui-icon-video { background-position: -224px -128px; } 218 | .ui-icon-script { background-position: -240px -128px; } 219 | .ui-icon-alert { background-position: 0 -144px; } 220 | .ui-icon-info { background-position: -16px -144px; } 221 | .ui-icon-notice { background-position: -32px -144px; } 222 | .ui-icon-help { background-position: -48px -144px; } 223 | .ui-icon-check { background-position: -64px -144px; } 224 | .ui-icon-bullet { background-position: -80px -144px; } 225 | .ui-icon-radio-off { background-position: -96px -144px; } 226 | .ui-icon-radio-on { background-position: -112px -144px; } 227 | .ui-icon-pin-w { background-position: -128px -144px; } 228 | .ui-icon-pin-s { background-position: -144px -144px; } 229 | .ui-icon-play { background-position: 0 -160px; } 230 | .ui-icon-pause { background-position: -16px -160px; } 231 | .ui-icon-seek-next { background-position: -32px -160px; } 232 | .ui-icon-seek-prev { background-position: -48px -160px; } 233 | .ui-icon-seek-end { background-position: -64px -160px; } 234 | .ui-icon-seek-start { background-position: -80px -160px; } 235 | /* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */ 236 | .ui-icon-seek-first { background-position: -80px -160px; } 237 | .ui-icon-stop { background-position: -96px -160px; } 238 | .ui-icon-eject { background-position: -112px -160px; } 239 | .ui-icon-volume-off { background-position: -128px -160px; } 240 | .ui-icon-volume-on { background-position: -144px -160px; } 241 | .ui-icon-power { background-position: 0 -176px; } 242 | .ui-icon-signal-diag { background-position: -16px -176px; } 243 | .ui-icon-signal { background-position: -32px -176px; } 244 | .ui-icon-battery-0 { background-position: -48px -176px; } 245 | .ui-icon-battery-1 { background-position: -64px -176px; } 246 | .ui-icon-battery-2 { background-position: -80px -176px; } 247 | .ui-icon-battery-3 { background-position: -96px -176px; } 248 | .ui-icon-circle-plus { background-position: 0 -192px; } 249 | .ui-icon-circle-minus { background-position: -16px -192px; } 250 | .ui-icon-circle-close { background-position: -32px -192px; } 251 | .ui-icon-circle-triangle-e { background-position: -48px -192px; } 252 | .ui-icon-circle-triangle-s { background-position: -64px -192px; } 253 | .ui-icon-circle-triangle-w { background-position: -80px -192px; } 254 | .ui-icon-circle-triangle-n { background-position: -96px -192px; } 255 | .ui-icon-circle-arrow-e { background-position: -112px -192px; } 256 | .ui-icon-circle-arrow-s { background-position: -128px -192px; } 257 | .ui-icon-circle-arrow-w { background-position: -144px -192px; } 258 | .ui-icon-circle-arrow-n { background-position: -160px -192px; } 259 | .ui-icon-circle-zoomin { background-position: -176px -192px; } 260 | .ui-icon-circle-zoomout { background-position: -192px -192px; } 261 | .ui-icon-circle-check { background-position: -208px -192px; } 262 | .ui-icon-circlesmall-plus { background-position: 0 -208px; } 263 | .ui-icon-circlesmall-minus { background-position: -16px -208px; } 264 | .ui-icon-circlesmall-close { background-position: -32px -208px; } 265 | .ui-icon-squaresmall-plus { background-position: -48px -208px; } 266 | .ui-icon-squaresmall-minus { background-position: -64px -208px; } 267 | .ui-icon-squaresmall-close { background-position: -80px -208px; } 268 | .ui-icon-grip-dotted-vertical { background-position: 0 -224px; } 269 | .ui-icon-grip-dotted-horizontal { background-position: -16px -224px; } 270 | .ui-icon-grip-solid-vertical { background-position: -32px -224px; } 271 | .ui-icon-grip-solid-horizontal { background-position: -48px -224px; } 272 | .ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; } 273 | .ui-icon-grip-diagonal-se { background-position: -80px -224px; } 274 | 275 | 276 | /* Misc visuals 277 | ----------------------------------*/ 278 | 279 | /* Corner radius */ 280 | .ui-corner-all, .ui-corner-top, .ui-corner-left, .ui-corner-tl { -moz-border-radius-topleft: 6px; -webkit-border-top-left-radius: 6px; -khtml-border-top-left-radius: 6px; border-top-left-radius: 6px; } 281 | .ui-corner-all, .ui-corner-top, .ui-corner-right, .ui-corner-tr { -moz-border-radius-topright: 6px; -webkit-border-top-right-radius: 6px; -khtml-border-top-right-radius: 6px; border-top-right-radius: 6px; } 282 | .ui-corner-all, .ui-corner-bottom, .ui-corner-left, .ui-corner-bl { -moz-border-radius-bottomleft: 6px; -webkit-border-bottom-left-radius: 6px; -khtml-border-bottom-left-radius: 6px; border-bottom-left-radius: 6px; } 283 | .ui-corner-all, .ui-corner-bottom, .ui-corner-right, .ui-corner-br { -moz-border-radius-bottomright: 6px; -webkit-border-bottom-right-radius: 6px; -khtml-border-bottom-right-radius: 6px; border-bottom-right-radius: 6px; } 284 | 285 | /* Overlays */ 286 | .ui-widget-overlay { background: #eeeeee url(images/ui-bg_diagonals-thick_90_eeeeee_40x40.png) 50% 50% repeat; opacity: .80;filter:Alpha(Opacity=80); } 287 | .ui-widget-shadow { margin: -7px 0 0 -7px; padding: 7px; background: #000000 url(images/ui-bg_highlight-hard_70_000000_1x100.png) 50% top repeat-x; opacity: .30;filter:Alpha(Opacity=30); -moz-border-radius: 8px; -khtml-border-radius: 8px; -webkit-border-radius: 8px; border-radius: 8px; }/* 288 | * jQuery UI Resizable 1.8.17 289 | * 290 | * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) 291 | * Dual licensed under the MIT or GPL Version 2 licenses. 292 | * http://jquery.org/license 293 | * 294 | * http://docs.jquery.com/UI/Resizable#theming 295 | */ 296 | .ui-resizable { position: relative;} 297 | .ui-resizable-handle { position: absolute;font-size: 0.1px;z-index: 99999; display: block; } 298 | .ui-resizable-disabled .ui-resizable-handle, .ui-resizable-autohide .ui-resizable-handle { display: none; } 299 | .ui-resizable-n { cursor: n-resize; height: 7px; width: 100%; top: -5px; left: 0; } 300 | .ui-resizable-s { cursor: s-resize; height: 7px; width: 100%; bottom: -5px; left: 0; } 301 | .ui-resizable-e { cursor: e-resize; width: 7px; right: -5px; top: 0; height: 100%; } 302 | .ui-resizable-w { cursor: w-resize; width: 7px; left: -5px; top: 0; height: 100%; } 303 | .ui-resizable-se { cursor: se-resize; width: 12px; height: 12px; right: 1px; bottom: 1px; } 304 | .ui-resizable-sw { cursor: sw-resize; width: 9px; height: 9px; left: -5px; bottom: -5px; } 305 | .ui-resizable-nw { cursor: nw-resize; width: 9px; height: 9px; left: -5px; top: -5px; } 306 | .ui-resizable-ne { cursor: ne-resize; width: 9px; height: 9px; right: -5px; top: -5px;}/* 307 | * jQuery UI Selectable 1.8.17 308 | * 309 | * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) 310 | * Dual licensed under the MIT or GPL Version 2 licenses. 311 | * http://jquery.org/license 312 | * 313 | * http://docs.jquery.com/UI/Selectable#theming 314 | */ 315 | .ui-selectable-helper { position: absolute; z-index: 100; border:1px dotted black; } 316 | /* 317 | * jQuery UI Accordion 1.8.17 318 | * 319 | * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) 320 | * Dual licensed under the MIT or GPL Version 2 licenses. 321 | * http://jquery.org/license 322 | * 323 | * http://docs.jquery.com/UI/Accordion#theming 324 | */ 325 | /* IE/Win - Fix animation bug - #4615 */ 326 | .ui-accordion { width: 100%; } 327 | .ui-accordion .ui-accordion-header { cursor: pointer; position: relative; margin-top: 1px; zoom: 1; } 328 | .ui-accordion .ui-accordion-li-fix { display: inline; } 329 | .ui-accordion .ui-accordion-header-active { border-bottom: 0 !important; } 330 | .ui-accordion .ui-accordion-header a { display: block; font-size: 1em; padding: .5em .5em .5em .7em; } 331 | .ui-accordion-icons .ui-accordion-header a { padding-left: 2.2em; } 332 | .ui-accordion .ui-accordion-header .ui-icon { position: absolute; left: .5em; top: 50%; margin-top: -8px; } 333 | .ui-accordion .ui-accordion-content { padding: 1em 2.2em; border-top: 0; margin-top: -2px; position: relative; top: 1px; margin-bottom: 2px; overflow: auto; display: none; zoom: 1; } 334 | .ui-accordion .ui-accordion-content-active { display: block; } 335 | /* 336 | * jQuery UI Autocomplete 1.8.17 337 | * 338 | * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) 339 | * Dual licensed under the MIT or GPL Version 2 licenses. 340 | * http://jquery.org/license 341 | * 342 | * http://docs.jquery.com/UI/Autocomplete#theming 343 | */ 344 | .ui-autocomplete { position: absolute; cursor: default; } 345 | 346 | /* workarounds */ 347 | * html .ui-autocomplete { width:1px; } /* without this, the menu expands to 100% in IE6 */ 348 | 349 | /* 350 | * jQuery UI Menu 1.8.17 351 | * 352 | * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) 353 | * Dual licensed under the MIT or GPL Version 2 licenses. 354 | * http://jquery.org/license 355 | * 356 | * http://docs.jquery.com/UI/Menu#theming 357 | */ 358 | .ui-menu { 359 | list-style:none; 360 | padding: 2px; 361 | margin: 0; 362 | display:block; 363 | float: left; 364 | } 365 | .ui-menu .ui-menu { 366 | margin-top: -3px; 367 | } 368 | .ui-menu .ui-menu-item { 369 | margin:0; 370 | padding: 0; 371 | zoom: 1; 372 | float: left; 373 | clear: left; 374 | width: 100%; 375 | } 376 | .ui-menu .ui-menu-item a { 377 | text-decoration:none; 378 | display:block; 379 | padding:.2em .4em; 380 | line-height:1.5; 381 | zoom:1; 382 | } 383 | .ui-menu .ui-menu-item a.ui-state-hover, 384 | .ui-menu .ui-menu-item a.ui-state-active { 385 | font-weight: normal; 386 | margin: -1px; 387 | } 388 | /* 389 | * jQuery UI Button 1.8.17 390 | * 391 | * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) 392 | * Dual licensed under the MIT or GPL Version 2 licenses. 393 | * http://jquery.org/license 394 | * 395 | * http://docs.jquery.com/UI/Button#theming 396 | */ 397 | .ui-button { display: inline-block; position: relative; padding: 0; margin-right: .1em; text-decoration: none !important; cursor: pointer; text-align: center; zoom: 1; overflow: visible; } /* the overflow property removes extra width in IE */ 398 | .ui-button-icon-only { width: 2.2em; } /* to make room for the icon, a width needs to be set here */ 399 | button.ui-button-icon-only { width: 2.4em; } /* button elements seem to need a little more width */ 400 | .ui-button-icons-only { width: 3.4em; } 401 | button.ui-button-icons-only { width: 3.7em; } 402 | 403 | /*button text element */ 404 | .ui-button .ui-button-text { display: block; line-height: 1.4; } 405 | .ui-button-text-only .ui-button-text { padding: .4em 1em; } 406 | .ui-button-icon-only .ui-button-text, .ui-button-icons-only .ui-button-text { padding: .4em; text-indent: -9999999px; } 407 | .ui-button-text-icon-primary .ui-button-text, .ui-button-text-icons .ui-button-text { padding: .4em 1em .4em 2.1em; } 408 | .ui-button-text-icon-secondary .ui-button-text, .ui-button-text-icons .ui-button-text { padding: .4em 2.1em .4em 1em; } 409 | .ui-button-text-icons .ui-button-text { padding-left: 2.1em; padding-right: 2.1em; } 410 | /* no icon support for input elements, provide padding by default */ 411 | input.ui-button { padding: .4em 1em; } 412 | 413 | /*button icon element(s) */ 414 | .ui-button-icon-only .ui-icon, .ui-button-text-icon-primary .ui-icon, .ui-button-text-icon-secondary .ui-icon, .ui-button-text-icons .ui-icon, .ui-button-icons-only .ui-icon { position: absolute; top: 50%; margin-top: -8px; } 415 | .ui-button-icon-only .ui-icon { left: 50%; margin-left: -8px; } 416 | .ui-button-text-icon-primary .ui-button-icon-primary, .ui-button-text-icons .ui-button-icon-primary, .ui-button-icons-only .ui-button-icon-primary { left: .5em; } 417 | .ui-button-text-icon-secondary .ui-button-icon-secondary, .ui-button-text-icons .ui-button-icon-secondary, .ui-button-icons-only .ui-button-icon-secondary { right: .5em; } 418 | .ui-button-text-icons .ui-button-icon-secondary, .ui-button-icons-only .ui-button-icon-secondary { right: .5em; } 419 | 420 | /*button sets*/ 421 | .ui-buttonset { margin-right: 7px; } 422 | .ui-buttonset .ui-button { margin-left: 0; margin-right: -.3em; } 423 | 424 | /* workarounds */ 425 | button.ui-button::-moz-focus-inner { border: 0; padding: 0; } /* reset extra padding in Firefox */ 426 | /* 427 | * jQuery UI Dialog 1.8.17 428 | * 429 | * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) 430 | * Dual licensed under the MIT or GPL Version 2 licenses. 431 | * http://jquery.org/license 432 | * 433 | * http://docs.jquery.com/UI/Dialog#theming 434 | */ 435 | .ui-dialog { position: absolute; padding: .2em; width: 300px; overflow: hidden; } 436 | .ui-dialog .ui-dialog-titlebar { padding: .4em 1em; position: relative; } 437 | .ui-dialog .ui-dialog-title { float: left; margin: .1em 16px .1em 0; } 438 | .ui-dialog .ui-dialog-titlebar-close { position: absolute; right: .3em; top: 50%; width: 19px; margin: -10px 0 0 0; padding: 1px; height: 18px; } 439 | .ui-dialog .ui-dialog-titlebar-close span { display: block; margin: 1px; } 440 | .ui-dialog .ui-dialog-titlebar-close:hover, .ui-dialog .ui-dialog-titlebar-close:focus { padding: 0; } 441 | .ui-dialog .ui-dialog-content { position: relative; border: 0; padding: .5em 1em; background: none; overflow: auto; zoom: 1; } 442 | .ui-dialog .ui-dialog-buttonpane { text-align: left; border-width: 1px 0 0 0; background-image: none; margin: .5em 0 0 0; padding: .3em 1em .5em .4em; } 443 | .ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset { float: right; } 444 | .ui-dialog .ui-dialog-buttonpane button { margin: .5em .4em .5em 0; cursor: pointer; } 445 | .ui-dialog .ui-resizable-se { width: 14px; height: 14px; right: 3px; bottom: 3px; } 446 | .ui-draggable .ui-dialog-titlebar { cursor: move; } 447 | /* 448 | * jQuery UI Slider 1.8.17 449 | * 450 | * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) 451 | * Dual licensed under the MIT or GPL Version 2 licenses. 452 | * http://jquery.org/license 453 | * 454 | * http://docs.jquery.com/UI/Slider#theming 455 | */ 456 | .ui-slider { position: relative; text-align: left; } 457 | .ui-slider .ui-slider-handle { position: absolute; z-index: 2; width: 1.2em; height: 1.2em; cursor: default; } 458 | .ui-slider .ui-slider-range { position: absolute; z-index: 1; font-size: .7em; display: block; border: 0; background-position: 0 0; } 459 | 460 | .ui-slider-horizontal { height: .8em; } 461 | .ui-slider-horizontal .ui-slider-handle { top: -.3em; margin-left: -.6em; } 462 | .ui-slider-horizontal .ui-slider-range { top: 0; height: 100%; } 463 | .ui-slider-horizontal .ui-slider-range-min { left: 0; } 464 | .ui-slider-horizontal .ui-slider-range-max { right: 0; } 465 | 466 | .ui-slider-vertical { width: .8em; height: 100px; } 467 | .ui-slider-vertical .ui-slider-handle { left: -.3em; margin-left: 0; margin-bottom: -.6em; } 468 | .ui-slider-vertical .ui-slider-range { left: 0; width: 100%; } 469 | .ui-slider-vertical .ui-slider-range-min { bottom: 0; } 470 | .ui-slider-vertical .ui-slider-range-max { top: 0; }/* 471 | * jQuery UI Tabs 1.8.17 472 | * 473 | * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) 474 | * Dual licensed under the MIT or GPL Version 2 licenses. 475 | * http://jquery.org/license 476 | * 477 | * http://docs.jquery.com/UI/Tabs#theming 478 | */ 479 | .ui-tabs { position: relative; padding: .2em; zoom: 1; } /* position: relative prevents IE scroll bug (element with position: relative inside container with overflow: auto appear as "fixed") */ 480 | .ui-tabs .ui-tabs-nav { margin: 0; padding: .2em .2em 0; } 481 | .ui-tabs .ui-tabs-nav li { list-style: none; float: left; position: relative; top: 1px; margin: 0 .2em 1px 0; border-bottom: 0 !important; padding: 0; white-space: nowrap; } 482 | .ui-tabs .ui-tabs-nav li a { float: left; padding: .5em 1em; text-decoration: none; } 483 | .ui-tabs .ui-tabs-nav li.ui-tabs-selected { margin-bottom: 0; padding-bottom: 1px; } 484 | .ui-tabs .ui-tabs-nav li.ui-tabs-selected a, .ui-tabs .ui-tabs-nav li.ui-state-disabled a, .ui-tabs .ui-tabs-nav li.ui-state-processing a { cursor: text; } 485 | .ui-tabs .ui-tabs-nav li a, .ui-tabs.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-selected a { cursor: pointer; } /* first selector in group seems obsolete, but required to overcome bug in Opera applying cursor: text overall if defined elsewhere... */ 486 | .ui-tabs .ui-tabs-panel { display: block; border-width: 0; padding: 1em 1.4em; background: none; } 487 | .ui-tabs .ui-tabs-hide { display: none !important; } 488 | /* 489 | * jQuery UI Datepicker 1.8.17 490 | * 491 | * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) 492 | * Dual licensed under the MIT or GPL Version 2 licenses. 493 | * http://jquery.org/license 494 | * 495 | * http://docs.jquery.com/UI/Datepicker#theming 496 | */ 497 | .ui-datepicker { width: 17em; padding: .2em .2em 0; display: none; } 498 | .ui-datepicker .ui-datepicker-header { position:relative; padding:.2em 0; } 499 | .ui-datepicker .ui-datepicker-prev, .ui-datepicker .ui-datepicker-next { position:absolute; top: 2px; width: 1.8em; height: 1.8em; } 500 | .ui-datepicker .ui-datepicker-prev-hover, .ui-datepicker .ui-datepicker-next-hover { top: 1px; } 501 | .ui-datepicker .ui-datepicker-prev { left:2px; } 502 | .ui-datepicker .ui-datepicker-next { right:2px; } 503 | .ui-datepicker .ui-datepicker-prev-hover { left:1px; } 504 | .ui-datepicker .ui-datepicker-next-hover { right:1px; } 505 | .ui-datepicker .ui-datepicker-prev span, .ui-datepicker .ui-datepicker-next span { display: block; position: absolute; left: 50%; margin-left: -8px; top: 50%; margin-top: -8px; } 506 | .ui-datepicker .ui-datepicker-title { margin: 0 2.3em; line-height: 1.8em; text-align: center; } 507 | .ui-datepicker .ui-datepicker-title select { font-size:1em; margin:1px 0; } 508 | .ui-datepicker select.ui-datepicker-month-year {width: 100%;} 509 | .ui-datepicker select.ui-datepicker-month, 510 | .ui-datepicker select.ui-datepicker-year { width: 49%;} 511 | .ui-datepicker table {width: 100%; font-size: .9em; border-collapse: collapse; margin:0 0 .4em; } 512 | .ui-datepicker th { padding: .7em .3em; text-align: center; font-weight: bold; border: 0; } 513 | .ui-datepicker td { border: 0; padding: 1px; } 514 | .ui-datepicker td span, .ui-datepicker td a { display: block; padding: .2em; text-align: right; text-decoration: none; } 515 | .ui-datepicker .ui-datepicker-buttonpane { background-image: none; margin: .7em 0 0 0; padding:0 .2em; border-left: 0; border-right: 0; border-bottom: 0; } 516 | .ui-datepicker .ui-datepicker-buttonpane button { float: right; margin: .5em .2em .4em; cursor: pointer; padding: .2em .6em .3em .6em; width:auto; overflow:visible; } 517 | .ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current { float:left; } 518 | 519 | /* with multiple calendars */ 520 | .ui-datepicker.ui-datepicker-multi { width:auto; } 521 | .ui-datepicker-multi .ui-datepicker-group { float:left; } 522 | .ui-datepicker-multi .ui-datepicker-group table { width:95%; margin:0 auto .4em; } 523 | .ui-datepicker-multi-2 .ui-datepicker-group { width:50%; } 524 | .ui-datepicker-multi-3 .ui-datepicker-group { width:33.3%; } 525 | .ui-datepicker-multi-4 .ui-datepicker-group { width:25%; } 526 | .ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header { border-left-width:0; } 527 | .ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header { border-left-width:0; } 528 | .ui-datepicker-multi .ui-datepicker-buttonpane { clear:left; } 529 | .ui-datepicker-row-break { clear:both; width:100%; font-size:0em; } 530 | 531 | /* RTL support */ 532 | .ui-datepicker-rtl { direction: rtl; } 533 | .ui-datepicker-rtl .ui-datepicker-prev { right: 2px; left: auto; } 534 | .ui-datepicker-rtl .ui-datepicker-next { left: 2px; right: auto; } 535 | .ui-datepicker-rtl .ui-datepicker-prev:hover { right: 1px; left: auto; } 536 | .ui-datepicker-rtl .ui-datepicker-next:hover { left: 1px; right: auto; } 537 | .ui-datepicker-rtl .ui-datepicker-buttonpane { clear:right; } 538 | .ui-datepicker-rtl .ui-datepicker-buttonpane button { float: left; } 539 | .ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current { float:right; } 540 | .ui-datepicker-rtl .ui-datepicker-group { float:right; } 541 | .ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header { border-right-width:0; border-left-width:1px; } 542 | .ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header { border-right-width:0; border-left-width:1px; } 543 | 544 | /* IE6 IFRAME FIX (taken from datepicker 1.5.3 */ 545 | .ui-datepicker-cover { 546 | display: none; /*sorry for IE5*/ 547 | display/**/: block; /*sorry for IE5*/ 548 | position: absolute; /*must have*/ 549 | z-index: -1; /*must have*/ 550 | filter: mask(); /*must have*/ 551 | top: -4px; /*must have*/ 552 | left: -4px; /*must have*/ 553 | width: 200px; /*must have*/ 554 | height: 200px; /*must have*/ 555 | }/* 556 | * jQuery UI Progressbar 1.8.17 557 | * 558 | * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) 559 | * Dual licensed under the MIT or GPL Version 2 licenses. 560 | * http://jquery.org/license 561 | * 562 | * http://docs.jquery.com/UI/Progressbar#theming 563 | */ 564 | .ui-progressbar { height:2em; text-align: left; overflow: hidden; } 565 | .ui-progressbar .ui-progressbar-value {margin: -1px; height:100%; } -------------------------------------------------------------------------------- /html/css/iiif.css: -------------------------------------------------------------------------------- 1 | 2 | label { 3 | margin-right: 5px; 4 | font-weight: normal !important; 5 | } 6 | 7 | input[type=text] { 8 | margin: 0; 9 | } 10 | 11 | ul { 12 | padding: 0; 13 | margin: 0; 14 | } 15 | 16 | li { 17 | list-style: square inside none; 18 | margin: 0; 19 | padding: 3px; 20 | } 21 | 22 | #header { 23 | background-color: #AED0EA; 24 | border-bottom: 1px solid #2779AA; 25 | color: #FFFFFF; 26 | margin: -10px -10px 0 -10px; 27 | height: 70px; 28 | } 29 | 30 | #uri { 31 | width: 250px; 32 | } 33 | 34 | #uuid { 35 | width: 300px; 36 | } 37 | 38 | #prefix { 39 | width: 100px; 40 | } 41 | 42 | .section { 43 | background-position: left top !important; 44 | margin-top: 10px; 45 | padding-left: 5px; 46 | float: left; 47 | clear: left; 48 | } 49 | 50 | .section h2 { 51 | font-size: 20px; 52 | padding-top: 5px; 53 | padding-left: 5px; 54 | } 55 | 56 | .content { 57 | float: left; 58 | margin: 5px 5px 5px 0; 59 | padding: 10px 15px; 60 | font-weight: normal; 61 | } 62 | 63 | .code { 64 | font-family: monospace; 65 | } 66 | 67 | #inputs .content { 68 | min-height: 45px; 69 | } 70 | 71 | #gen_img, #run_tests { 72 | font-size: 16px; 73 | } 74 | 75 | #run_tests { 76 | margin-top: 30px; 77 | } 78 | 79 | #inputs .input label { 80 | min-width: 60px; 81 | text-align: right; 82 | display: inline-block; 83 | } 84 | 85 | #inputs ins { 86 | display: inline-block; 87 | margin-left: -5px; 88 | cursor: pointer; 89 | } 90 | 91 | .category { 92 | float: left; 93 | padding: 5px 15px 5px 0; 94 | } 95 | 96 | .input { 97 | margin-top: 5px; 98 | } 99 | 100 | 101 | 102 | #results .content { 103 | min-width: 300px; 104 | } 105 | 106 | #resultsBack { 107 | float: left; 108 | } 109 | 110 | #resultDetails { 111 | clear: left; 112 | } 113 | 114 | .result { 115 | margin: 5px 0; 116 | padding: 5px; 117 | } 118 | 119 | .resultLabel { 120 | font-weight: bold; 121 | } 122 | 123 | .result span { 124 | display: block; 125 | } 126 | 127 | .pass { 128 | background-color: #70E070; 129 | } 130 | 131 | .fail { 132 | background-color: #F06060; 133 | } 134 | -------------------------------------------------------------------------------- /html/download.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: IIIF Image API Validator 3 | title_override: IIIF Image API Validator 4 | id: technical-details 5 | categories: [pages] 6 | layout: sub-page 7 | --- 8 | 9 | 10 | 11 |
12 |
13 |

The IIIF Image API validator runs a set of tests against your implementation. 14 |

You must first download the test image (by clicking the button below) then save and import the test image into your repository, to be served through your IIIF Image service. Once the image is accessible through your IIIF service, you may then run the tests in the validator. Fill out server with the hostname of your server, prefix with everything in the URI path up to the identifier, and your identifier for the test image. 15 |

16 |

Click to download a test image below.

17 | 21 |

The test images may be freely reused under CC0.

22 |
23 | 24 |

25 | 26 |
27 | Technical Note 28 |

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

31 |
    32 |
  • Download the code from github and run it locally.
  • 33 |
  • Use it online with JSON based output, by an HTTP GET endpoints following this pattern:
    34 | http://iiif.io/api/image/validator/service/test-name?server=server-here&prefix=prefix-here&identifier=identifier-here&version=2.0
    35 | The list of tests is available at:
    36 | http://iiif.io/api/image/validator/service/list_tests 37 |
  • 38 |
39 |
40 |
41 | 42 | -------------------------------------------------------------------------------- /html/img/iiif-logo-60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/image-validator/6d7f3e16871aefa451ed398d90743e3c218fe5be/html/img/iiif-logo-60.png -------------------------------------------------------------------------------- /html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | IIIF Image API Validator 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 |
17 |
18 |

IIIF Server to Validate

19 |
20 | 21 | 22 | 23 |
24 |
25 | 26 | 27 | 28 |
29 |
30 | 31 | 32 | 33 |
34 |
35 |
36 |

Select Version and Level

37 |
38 | 44 |
45 |
46 | 53 |
54 | 57 | 58 |
59 |
60 | 61 |
62 |
63 |
64 |
65 |

Tests

66 |
67 |
68 |
69 |
70 |
71 |
72 | 73 | 74 | -------------------------------------------------------------------------------- /html/js/results.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | function Tester() { 5 | this.baseUrl = 'http://iiif.io/api/image/validator/service/'; 6 | 7 | this.categories = { 8 | 1: 'Info/Identifier', 9 | 2: 'Region', 10 | 3: 'Size', 11 | 4: 'Rotation', 12 | 5: 'Quality', 13 | 6: 'Format' 14 | }; 15 | 16 | this.tests = {}; 17 | this.currentTests = []; 18 | this.cancelled = false; 19 | } 20 | 21 | Tester.prototype.getQueryParams = function () { 22 | var match, 23 | pl = /\+/g, // Regex for replacing addition symbol with a space 24 | search = /([^&=]+)=?([^&]*)/g, 25 | decode = function (s) { return decodeURIComponent(s.replace(pl, " ")); }, 26 | query = window.location.search.substring(1), 27 | urlParams = {}; 28 | 29 | while (match = search.exec(query)) 30 | urlParams[decode(match[1])] = decode(match[2]); 31 | 32 | return urlParams; 33 | } 34 | 35 | 36 | Tester.prototype.runTests = function(uri, prefix, imageId, version) { 37 | this.cancelled = false; 38 | var totalTests = this.currentTests.length; 39 | var testsPassed = 0; 40 | 41 | $('#results h1').html('Results'); 42 | 43 | var resultsContainer = $('#results'); 44 | resultsContainer.empty(); 45 | 46 | for (var i = 0; i < this.currentTests.length; i++) { 47 | var t = this.currentTests[i]; 48 | //var test = this.tests[t]; 49 | //var label = test.label || t; 50 | resultsContainer.append('
'+ 51 | ''+ t +''+ 52 | '
'); 53 | } 54 | 55 | function doTest(test) { 56 | if (test) { 57 | var testStart = new Date().getTime(); 58 | $.ajax({ 59 | url: this.baseUrl+test, 60 | data: { 61 | version: version, 62 | server: uri, 63 | prefix: prefix, 64 | identifier: imageId, 65 | t: testStart 66 | }, 67 | dataType: 'json', 68 | success: $.proxy(function(data, status, xhr) { 69 | var testFinish = new Date().getTime(); 70 | var result = $('#r_'+test); 71 | var elapsed = testFinish - testStart; 72 | result.find('.elapsed').html('Elapsed time (ms): '+elapsed); 73 | result.find('.resultLabel').html(data.label); 74 | 75 | var message = '
    '; 76 | if (data.status) { 77 | if (data.status == 'success') { 78 | result.addClass('pass'); 79 | testsPassed++; 80 | } else { 81 | result.addClass('fail'); 82 | } 83 | for (var key in data) { 84 | if (key != 'status' && key != 'test' && key != 'label') { 85 | message += '
  • '+key+': '+data[key]+'
  • '; 86 | } 87 | } 88 | } else { 89 | result.addClass('fail'); 90 | message = '
  • Message: '+status+'
  • '; 91 | } 92 | message += '
'; 93 | result.find('.message').html(message); 94 | 95 | $.proxy(doTest, this)(this.currentTests.shift()); 96 | }, this), 97 | error: $.proxy(function(xhr, status, error) { 98 | var testFinish = new Date().getTime(); 99 | var result = $('#r_'+test); 100 | var elapsed = testFinish - testStart; 101 | 102 | result.find('.elapsed').html('Elapsed time (ms): '+elapsed); 103 | result.addClass('fail'); 104 | var message = '
  • Message: '+status+'
'; 105 | result.find('.message').html(message); 106 | 107 | $.proxy(doTest, this)(this.currentTests.shift()); 108 | }, this) 109 | }); 110 | } else { 111 | if (!this.cancelled) { 112 | var details = '
'+testsPassed+' out of '+totalTests+' tests passed.'; 113 | var result; 114 | if (testsPassed == totalTests) { 115 | result = 'passed'; 116 | $('#results h1').append(': Pass'+details); 117 | } else { 118 | result = 'failed'; 119 | $('#results h1').append(': Fail'+details); 120 | } 121 | var level = $('#level').val(); 122 | if (level == '-1') level = 'Custom'; 123 | var msg = 'You '+result+' IIIF Compliance Level: '+level+'.'+details; 124 | // this.showMessage('Result', msg); 125 | } 126 | } 127 | } 128 | 129 | $.proxy(doTest, this)(this.currentTests.shift()); 130 | }; 131 | 132 | Tester.prototype.cancelTests = function() { 133 | this.currentTests = []; 134 | this.cancelled = true; 135 | }; 136 | 137 | Tester.prototype.init = function() { 138 | 139 | // Get tests and data from query 140 | var params = this.getQueryParams(); 141 | var server = params['server'], 142 | prefix = params['prefix'], 143 | id = params['identifier'], 144 | version = params['version'], 145 | tests = []; 146 | 147 | for (var key in params) { 148 | if (params.hasOwnProperty(key) && params[key] == "on") { 149 | tests.push(key); 150 | } 151 | } 152 | this.currentTests = tests; 153 | this.runTests(server, prefix, id, version); 154 | }; 155 | 156 | $(document).ready(function() { 157 | tester = new Tester(); 158 | tester.init(); 159 | }); 160 | -------------------------------------------------------------------------------- /html/js/select.js: -------------------------------------------------------------------------------- 1 | function Tester() { 2 | this.baseUrl = 'http://iiif.io/api/image/validator/service/'; 3 | 4 | this.categories = { 5 | 1: 'Info/Identifier', 6 | 2: 'Region', 7 | 3: 'Size', 8 | 4: 'Rotation', 9 | 5: 'Quality', 10 | 6: 'Format', 11 | 7: 'HTTP' 12 | }; 13 | 14 | this.tests = {}; 15 | this.currentTests = []; 16 | this.cancelled = false; 17 | } 18 | 19 | Tester.prototype.doLevelCheck = function(level) { 20 | for (var t in this.tests) { 21 | var test = this.tests[t]; 22 | $('#'+t).prop('checked', test.level <= level); 23 | } 24 | }; 25 | 26 | Tester.prototype.showMessage = function(title, msg) { 27 | $('#dialog').dialog('option', 'title', title); 28 | $('#dialog p').html(msg); 29 | $('#dialog').dialog('open'); 30 | }; 31 | 32 | Tester.prototype.fetchTestList = function() { 33 | var _this = this; 34 | var testContainer = $('#tests > div'); 35 | testContainer.empty(); 36 | for (var c in this.categories) { 37 | var name = this.categories[c]; 38 | testContainer.append('

'+name+'

'); 39 | } 40 | 41 | $.ajax({ 42 | url: this.baseUrl+"list_tests", 43 | data: { 44 | version: $("#version").val(), 45 | t: new Date().getTime() 46 | }, 47 | dataType: 'json', 48 | success: function(data, status, xhr) { 49 | 50 | _this.tests = data; 51 | 52 | ltests = []; 53 | for (var t in data) { 54 | if (data.hasOwnProperty(t)) { 55 | test = data[t]; 56 | test.id = t; 57 | ltests.push(test); 58 | } 59 | } 60 | ltests.sort(function(a,b) {return a.level - b.level}); 61 | 62 | // sort tests by level 63 | 64 | for (var t=0, test; test = ltests[t]; t++) { 65 | var label = test.label || test.id; 66 | $('#c_'+test.category).append('
'+ 67 | ''+ 68 | '
'); 69 | } 70 | $('#tests input').click(function(ev) { 71 | $('#level').val('-1'); 72 | }); 73 | $('#level').val('1'); 74 | _this.doLevelCheck(1); 75 | } 76 | }); 77 | } 78 | 79 | Tester.prototype.init = function() { 80 | 81 | this.fetchTestList(); 82 | 83 | $('#dialog').dialog({ 84 | autoOpen: false, 85 | modal: true, 86 | height: 250, 87 | width: 270, 88 | buttons: { 89 | Ok: function() { 90 | $(this).dialog('close'); 91 | } 92 | } 93 | }); 94 | 95 | $('#inputs ins').each(function(index, el) { 96 | var msg = ''; 97 | switch (index) { 98 | case 0: 99 | msg = 'The base URI for your IIIF implementation, e.g. http://shared-canvas.org.'; 100 | break; 101 | case 1: 102 | msg = 'The prefix for your IIIF implementation, e.g. iiif.'; 103 | break; 104 | case 2: 105 | msg = 'The filename for a test image you generated from the "Generate Test Image" tab and saved to your server. Leave off the .png extension. The ID should look something like: f8c6e480-f75d-11e1-b397-0011259ed879'; 106 | } 107 | $(el).data('msg', msg); 108 | }).click($.proxy(function(ev) { 109 | this.showMessage('Help', $(ev.target).data('msg')); 110 | }, this)); 111 | 112 | $('#level').change($.proxy(function(ev) { 113 | var level = parseInt($(ev.target).val()); 114 | this.doLevelCheck(level); 115 | }, this)); 116 | 117 | $('#version').change($.proxy(function(ev) { 118 | // regenerate test options 119 | this.fetchTestList(); 120 | }, this)); 121 | 122 | $('#run_tests').click($.proxy(function(ev) { 123 | var errors = ''; 124 | 125 | var uri = $('#server').val(); 126 | if (uri == '') { 127 | errors += 'The Server field is empty.\n'; 128 | } 129 | 130 | var prefix = $('#prefix').val(); 131 | if (prefix == '') { 132 | errors += 'The Prefix field is empty.\n'; 133 | } 134 | 135 | var id = $('#identifier').val(); 136 | if (errors == '') { 137 | $('#tabs').hide(); 138 | $('#results').show(); 139 | this.runTests(uri, prefix, id); 140 | } else { 141 | this.showMessage('Error', errors); 142 | } 143 | 144 | }, this)); 145 | 146 | }; 147 | 148 | $(document).ready(function() { 149 | tester = new Tester(); 150 | tester.init(); 151 | }); 152 | -------------------------------------------------------------------------------- /html/results.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | IIIF Image API Validator Results 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 20 |
21 | 22 |
23 |
24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /iiif-validate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Run validator code from command line. 3 | 4 | Wrapper around validator.py for use in local manual and continuous 5 | integration tests of IIIF servers. Command line options specify 6 | parameters of the server, the API version to be tested and the 7 | expected compliance level. Exit code is zero for success, non-zero 8 | otherwise (number of failed tests). 9 | """ 10 | 11 | from iiif_validator.validator import ValidationInfo,TestSuite,ImageAPI 12 | import logging 13 | import optparse 14 | import sys 15 | import traceback 16 | 17 | # Options and arguments 18 | p = optparse.OptionParser(description='IIIF Command Line Validator', 19 | usage='usage: %prog -s SERVER -p PREFIX -i IDENTIFIER [options] (-h for help)') 20 | p.add_option('--identifier','-i', action='store', 21 | help="identifier to run tests for") 22 | p.add_option('--server','-s', action='store', default='localhost:8000', 23 | help="server name of IIIF service, including port if not port 80 (default localhost:8000)") 24 | p.add_option('--prefix','-p', action='store', default='', 25 | help="prefix of IIIF service on server (default none)") 26 | p.add_option('--scheme', action='store', default='http', 27 | help="scheme (http or https, default http)") 28 | p.add_option('--auth','-a', action='store', default='', 29 | help="auth info for service (default none)") 30 | p.add_option('--version', action='store', default='2.0', 31 | help="IIIF API version to test for (default 2.0)") 32 | p.add_option('--level', action='store', type='int', default=1, 33 | help="compliance level to test (default 1)") 34 | p.add_option('--test', action='append', type='string', 35 | help="run specific named tests, ignores --level (repeatable)") 36 | p.add_option('--verbose', '-v', action='store_true', 37 | help="be verbose") 38 | p.add_option('--quiet','-q', action='store_true', 39 | help="minimal output only for errors") 40 | (opt, args) = p.parse_args() 41 | 42 | # Logging/output 43 | level = (logging.INFO if opt.verbose else (logging.ERROR if opt.quiet else logging.WARNING)) 44 | logging.basicConfig(level=level,format='%(message)s') 45 | 46 | # Sanity check 47 | if (not opt.identifier): 48 | logging.error("No identifier specified, aborting (-h for help)") 49 | exit(99) 50 | 51 | # Run as one shot set of tests with output to stdout 52 | info2 = ValidationInfo() 53 | tests = TestSuite(info2).list_tests(opt.version) 54 | n = 0 55 | bad = 0 56 | for testname in tests: 57 | if (opt.test): 58 | if (testname not in opt.test): 59 | continue 60 | elif (tests[testname]['level']>opt.level): 61 | continue 62 | n += 1 63 | test_str = ("[%d] test %s" % (n,testname)) 64 | try: 65 | info = ValidationInfo() 66 | testSuite = TestSuite(info) 67 | result = ImageAPI(opt.identifier, opt.server, opt.prefix, opt.scheme, 68 | opt.auth, opt.version, debug=False) 69 | testSuite.run_test(testname, result) 70 | if result.exception: 71 | e = result.exception 72 | bad += 1 73 | logging.error("%s FAIL"%test_str) 74 | logging.error(" url: %s\n got: %s\n expected: %s\n type: %s\n message: %s\n Is Warning?: %s"%(result.urls,e.got,e.expected,e.type, e.message, e.warning)) 75 | else: 76 | logging.warning("%s PASS"%test_str) 77 | logging.info(" url: %s\n tests: %s\n"%(result.urls,result.tests)) 78 | except Exception as e: 79 | bad += 1 80 | trace=traceback.format_exc() 81 | logging.error("%s FAIL"%test_str) 82 | logging.error(" exception: %s\n"%(str(e))) 83 | logging.info(trace) 84 | logging.warning("Done (%d tests, %d failures)" % (n,bad)) 85 | exit(bad) 86 | -------------------------------------------------------------------------------- /iiif-validator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Minimal wrapper around iiif-validator/validator.py to run server 3 | from the command line, or from mod_wsgi in apache 4 | """ 5 | 6 | from iiif_validator.validator import main, apache 7 | if __name__ == "__main__": 8 | main() 9 | else: 10 | application = apache() 11 | -------------------------------------------------------------------------------- /iiif_validator/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/image-validator/6d7f3e16871aefa451ed398d90743e3c218fe5be/iiif_validator/__init__.py -------------------------------------------------------------------------------- /iiif_validator/apache.py: -------------------------------------------------------------------------------- 1 | """Minimal wrapper around validator to set up for apache 2 | """ 3 | from .validator import apache 4 | application = apache() 5 | -------------------------------------------------------------------------------- /iiif_validator/tests/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | __author__ = "IIIF Contributors" 3 | 4 | # List all modules that subclass BaseTest to implement a test 5 | from . import baseurl_redirect,cors,format_conneg,format_error_random,format_gif,\ 6 | format_jp2,format_jpg,format_pdf,format_png,format_tif,format_webp,id_basic,\ 7 | id_error_escapedslash,id_error_random,id_error_unescaped,id_escaped,id_squares,\ 8 | info_json,info_xml,jsonld,linkheader_canonical,linkheader_profile,quality_bitonal,\ 9 | quality_color,quality_error_random,quality_grey,region_error_random,region_percent,\ 10 | region_pixels,rot_error_random,rot_full_basic,rot_full_non90,rot_mirror,\ 11 | rot_mirror_180,rot_region_basic,rot_region_non90,size_bwh,size_ch,size_error_random,\ 12 | size_percent,size_region,size_up,size_noup,size_wc,size_wh, size_nofull, region_square 13 | -------------------------------------------------------------------------------- /iiif_validator/tests/baseurl_redirect.py: -------------------------------------------------------------------------------- 1 | from .test import BaseTest, ValidatorError 2 | try: 3 | # python3 4 | from urllib.request import Request, urlopen, HTTPError 5 | print ('Importing 3') 6 | except ImportError: 7 | # fall back to python2 8 | from urllib2 import Request, urlopen, HTTPError 9 | print ('Importing 2') 10 | 11 | 12 | class Test_Baseurl_Redirect(BaseTest): 13 | label = 'Base URL Redirects' 14 | level = 1 15 | category = 7 16 | versions = [u'2.0', u'3.0'] 17 | validationInfo = None 18 | 19 | def run(self, result): 20 | url = result.make_info_url() 21 | url = url.replace('/info.json', '') 22 | newurl = '' 23 | try: 24 | r = Request(url) 25 | wh = urlopen(r) 26 | img = wh.read() 27 | wh.close() 28 | newurl = wh.geturl() 29 | except HTTPError as e: 30 | wh = e 31 | if wh.getcode() >= 300 and wh.getcode() < 400: 32 | newurl = wh.headers['Location'] 33 | else: 34 | newurl = wh.geturl() 35 | except Exception as error: 36 | raise ValidatorError('url-check', str(error), 301, result, 'Failed to redirect from url: {}.'.format(url)) 37 | 38 | if newurl == url: 39 | print (wh) 40 | print (wh.geturl()) 41 | print (type(wh)) 42 | # we didn't redirect 43 | raise ValidatorError('redirect', newurl, '{}/info.json'.format(url), result, 'Failed to redirect from {} to {}/info.json. Response code {}'.format(newurl, url, wh.getcode())) 44 | else: 45 | # we must have redirected if our url is not what was requested 46 | result.tests.append('redirect') 47 | return result 48 | -------------------------------------------------------------------------------- /iiif_validator/tests/cors.py: -------------------------------------------------------------------------------- 1 | from .test import BaseTest 2 | 3 | class Test_Cors(BaseTest): 4 | label = 'Cross Origin Headers' 5 | level = 1 6 | category = 7 7 | versions = [u'1.0', u'1.1', u'2.0', u'3.0'] 8 | validationInfo = None 9 | 10 | def run(self, result): 11 | info = result.get_info(); 12 | cors = '' 13 | for k,v in result.last_headers.items(): 14 | if k.lower() == 'access-control-allow-origin': 15 | cors = v 16 | break 17 | self.validationInfo.check('CORS', cors, '*', result, 'Failed to get correct CORS header.') 18 | return result 19 | -------------------------------------------------------------------------------- /iiif_validator/tests/format_conneg.py: -------------------------------------------------------------------------------- 1 | from .test import BaseTest 2 | try: 3 | # python3 4 | from urllib.request import Request, urlopen, HTTPError 5 | except ImportError: 6 | # fall back to python2 7 | from urllib2 import Request, urlopen, HTTPError 8 | 9 | class Test_Format_Conneg(BaseTest): 10 | label = 'Negotiated format' 11 | level = 1 12 | category = 7 13 | versions = [u'1.0', u'1.1'] 14 | validationInfo = None 15 | 16 | def run(self, result): 17 | url = result.make_url(params={}) 18 | hdrs = {'Accept': 'image/png;q=1.0'} 19 | try: 20 | r = Request(url, headers=hdrs) 21 | wh = urlopen(r) 22 | img = wh.read() 23 | wh.close() 24 | except HTTPError as e: 25 | wh = e 26 | ct = wh.headers['content-type'] 27 | result.last_url = url 28 | try: # py2 29 | result.last_headers = wh.headers.dict 30 | except: 31 | result.last_headers = wh.info() 32 | result.last_status = wh.code 33 | result.urls.append(url) 34 | self.validationInfo.check('format', ct, 'image/png', result) 35 | return result -------------------------------------------------------------------------------- /iiif_validator/tests/format_error_random.py: -------------------------------------------------------------------------------- 1 | from .test import BaseTest, ValidatorError 2 | 3 | class Test_Format_Error_Random(BaseTest): 4 | label = 'Random format gives 400' 5 | level = 1 6 | category = 6 7 | versions = [u'1.0', u'1.1', u'2.0', u'3.0'] 8 | validationInfo = None 9 | 10 | def run(self, result): 11 | url = result.make_url({'format': self.validationInfo.make_randomstring(3)}) 12 | try: 13 | error = result.fetch(url) 14 | self.validationInfo.check('status', result.last_status, [400, 415, 503], result) 15 | return result 16 | except Exception as error: 17 | raise ValidatorError('url-check', str(error), 400, result, 'Failed to get random format from url: {}.'.format(url)) 18 | -------------------------------------------------------------------------------- /iiif_validator/tests/format_gif.py: -------------------------------------------------------------------------------- 1 | from .test import BaseTest 2 | 3 | class Test_Format_Gif(BaseTest): 4 | label = 'GIF format' 5 | level = 3 6 | category = 6 7 | versions = [u'1.0', u'1.1', u'2.0', u'3.0'] 8 | validationInfo = None 9 | 10 | def run(self, result): 11 | try: 12 | params = {'format': 'gif'} 13 | img = result.get_image(params) 14 | self.validationInfo.check('quality', img.format, 'GIF', result) 15 | return result 16 | except: 17 | self.validationInfo.check('status', result.last_status, 200, result) 18 | raise 19 | -------------------------------------------------------------------------------- /iiif_validator/tests/format_jp2.py: -------------------------------------------------------------------------------- 1 | from .test import BaseTest, ValidatorError 2 | import magic, urllib 3 | 4 | try: 5 | # python3 6 | from urllib.request import Request, urlopen, HTTPError 7 | except ImportError: 8 | # fall back to python2 9 | from urllib2 import Request, urlopen, HTTPError 10 | 11 | class Test_Format_Jp2(BaseTest): 12 | label = 'JPEG2000 format' 13 | level = {u'3.0': 3, u'2.0': 3, u'1.0': 2, u'1.1': 3} 14 | category = 6 15 | versions = [u'1.0', u'1.1', u'2.0', u'3.0'] 16 | validationInfo = None 17 | 18 | def run(self, result): 19 | 20 | params = {'format': 'jp2'} 21 | url = result.make_url(params) 22 | # Need as plain string for magic 23 | try: 24 | wh = urlopen(url) 25 | except HTTPError as error: 26 | raise ValidatorError('format', 'http response code: {}'.format(error.code), url, result, 'Failed to retrieve jp2, got response code {}'.format(error.code)) 27 | img = wh.read() 28 | wh.close() 29 | # check response code before checking the file 30 | if wh.getcode() != 200: 31 | raise ValidatorError('format', 'http response code: {}'.format(wh.getcode()), url, result, 'Failed to retrieve jp2, got response code {}'.format(wh.getcode())) 32 | 33 | with magic.Magic() as m: 34 | print ('test') 35 | info = m.id_buffer(img) 36 | if not info.startswith('JPEG 2000'): 37 | # Not JP2 38 | raise ValidatorError('format', info, 'JPEG 2000', result) 39 | else: 40 | result.tests.append('format') 41 | return result 42 | -------------------------------------------------------------------------------- /iiif_validator/tests/format_jpg.py: -------------------------------------------------------------------------------- 1 | from .test import BaseTest 2 | 3 | class Test_Format_Jpg(BaseTest): 4 | label = 'JPG format' 5 | # IIIF version and complaince level where jpg became manditory 6 | level = {u'3.0': 0, u'2.0': 0, u'1.0': 1, u'1.1': 1} 7 | category = 6 8 | versions = [u'1.0', u'1.1', u'2.0', u'3.0'] 9 | validationInfo = None 10 | 11 | def run(self, result): 12 | try: 13 | params = {'format': 'jpg'} 14 | img = result.get_image(params) 15 | self.validationInfo.check('quality', img.format, 'JPEG', result) 16 | return result 17 | except: 18 | self.validationInfo.check('status', result.last_status, 200, result) 19 | raise 20 | -------------------------------------------------------------------------------- /iiif_validator/tests/format_pdf.py: -------------------------------------------------------------------------------- 1 | from .test import BaseTest, ValidatorError 2 | import magic 3 | try: 4 | # python3 5 | from urllib.request import Request, urlopen, HTTPError 6 | except ImportError: 7 | # fall back to python2 8 | from urllib2 import Request, urlopen, HTTPError 9 | 10 | class Test_Format_Pdf(BaseTest): 11 | label = 'PDF format' 12 | level = 3 13 | category = 6 14 | versions = [u'1.0', u'1.1', u'2.0', u'3.0'] 15 | validationInfo = None 16 | 17 | def run(self, result): 18 | 19 | params = {'format': 'pdf'} 20 | url = result.make_url(params) 21 | # Need as plain string for magic 22 | try: 23 | wh = urlopen(url) 24 | except HTTPError as error: 25 | raise ValidatorError('format', 'http response code: {}'.format(error.code), url, result, 'Failed to retrieve pdf, got response code {}'.format(error.code)) 26 | img = wh.read() 27 | wh.close() 28 | # check response code before checking the file 29 | if wh.getcode() != 200: 30 | raise ValidatorError('format', 'http response code: {}'.format(wh.getcode()), url, result, 'Failed to retrieve pdf, got response code {}'.format(wh.getcode())) 31 | 32 | with magic.Magic() as m: 33 | info = m.id_buffer(img) 34 | if not info.startswith('PDF document'): 35 | # Not JP2 36 | raise ValidatorError('format', info, 'PDF', result) 37 | else: 38 | result.tests.append('format') 39 | return result 40 | -------------------------------------------------------------------------------- /iiif_validator/tests/format_png.py: -------------------------------------------------------------------------------- 1 | from .test import BaseTest 2 | 3 | class Test_Format_Png(BaseTest): 4 | label = 'PNG format' 5 | level = 2 6 | category = 6 7 | versions = [u'1.0', u'1.1', u'2.0', u'3.0'] 8 | validationInfo = None 9 | 10 | def run(self, result): 11 | try: 12 | params = {'format': 'png'} 13 | img = result.get_image(params) 14 | self.validationInfo.check('quality', img.format, 'PNG', result) 15 | return result 16 | except: 17 | self.validationInfo.check('status', result.last_status, 200, result) 18 | raise 19 | -------------------------------------------------------------------------------- /iiif_validator/tests/format_tif.py: -------------------------------------------------------------------------------- 1 | from .test import BaseTest 2 | 3 | class Test_Format_Tif(BaseTest): 4 | label = 'TIFF format' 5 | level = 3 6 | category = 6 7 | versions = [u'1.0', u'1.1', u'2.0', u'3.0'] 8 | validationInfo = None 9 | 10 | def run(self, result): 11 | try: 12 | params = {'format': 'tif'} 13 | img = result.get_image(params) 14 | self.validationInfo.check('quality', img.format, 'TIFF', result) 15 | return result 16 | except: 17 | self.validationInfo.check('status', result.last_status, 200, result,'Failed to retrieve tiff.') 18 | raise 19 | -------------------------------------------------------------------------------- /iiif_validator/tests/format_webp.py: -------------------------------------------------------------------------------- 1 | from .test import BaseTest, ValidatorError 2 | try: 3 | # python3 4 | from urllib.request import Request, urlopen, HTTPError 5 | except ImportError: 6 | # fall back to python2 7 | from urllib2 import Request, urlopen, HTTPError 8 | 9 | class Test_Format_Webp(BaseTest): 10 | label = 'WebP format' 11 | level = 3 12 | category = 6 13 | versions = [u'2.0', u'3.0'] 14 | validationInfo = None 15 | 16 | def run(self, result): 17 | 18 | # chrs 8:12 == "WEBP" 19 | params = {'format': 'webp'} 20 | url = result.make_url(params) 21 | # Need as plain string for magic 22 | try: 23 | wh = urlopen(url) 24 | except HTTPError as error: 25 | raise ValidatorError('format', 'http response code: {}'.format(error.code), url, result, 'Failed to retrieve webp, got response code {}'.format(error.code)) 26 | img = wh.read() 27 | wh.close() 28 | if img[8:12] != "WEBP": 29 | raise ValidatorError('format', 'unknown', 'WEBP', result) 30 | else: 31 | result.tests.append('format') 32 | return result 33 | -------------------------------------------------------------------------------- /iiif_validator/tests/id_basic.py: -------------------------------------------------------------------------------- 1 | from .test import BaseTest, ValidatorError 2 | 3 | class Test_Id_Basic(BaseTest): 4 | label = 'Image is returned' 5 | level = 0 6 | category = 1 7 | versions = [u'1.0', u'1.1', u'2.0', u'3.0'] 8 | validationInfo = None 9 | 10 | def run(self, result): 11 | url = result.make_url(params={}) 12 | try: 13 | data = result.fetch(url) 14 | self.validationInfo.check('status', result.last_status, 200, result) 15 | img = result.make_image(data) 16 | return result 17 | except: 18 | raise ValidatorError('status', result.last_status, 200, result, 'Failed to retrieve url: {}'.format(url)) 19 | -------------------------------------------------------------------------------- /iiif_validator/tests/id_error_escapedslash.py: -------------------------------------------------------------------------------- 1 | from .test import BaseTest, ValidatorError 2 | 3 | class Test_Id_Error_Escapedslash(BaseTest): 4 | label = 'Forward slash gives 404' 5 | level = 1 # should this also be true for level0? Glen 6 | category = 1 7 | versions = [u'1.0', u'1.1', u'2.0',u'3.0'] 8 | validationInfo = None 9 | 10 | def run(self, result): 11 | try: 12 | url = result.make_url({'identifier': 'a/b'}) 13 | error = result.fetch(url) 14 | self.validationInfo.check('status', result.last_status, 404, result) 15 | return result 16 | except Exception as error: 17 | raise ValidatorError('url-check', str(error), 404, result, 'Failed to get random identifier from url {}.'.format(url)) 18 | -------------------------------------------------------------------------------- /iiif_validator/tests/id_error_random.py: -------------------------------------------------------------------------------- 1 | from .test import BaseTest, ValidatorError 2 | import uuid 3 | 4 | class Test_Id_Error_Random(BaseTest): 5 | label = 'Random identifier gives 404' 6 | level = 1 7 | category = 1 8 | versions = [u'1.0', u'1.1', u'2.0', u'3.0'] 9 | validationInfo = None 10 | 11 | def run(self, result): 12 | try: 13 | url = result.make_url({'identifier': str(uuid.uuid1())}) 14 | error = result.fetch(url) 15 | self.validationInfo.check('status', result.last_status, 404, result) 16 | return result 17 | except Exception as error: 18 | raise ValidatorError('url-check', str(error), 404, result, 'Failed to get random identifier from url: {}.'.format(url)) 19 | -------------------------------------------------------------------------------- /iiif_validator/tests/id_error_unescaped.py: -------------------------------------------------------------------------------- 1 | from .test import BaseTest, ValidatorError 2 | 3 | class Test_Id_Error_Unescaped(BaseTest): 4 | label = 'Unescaped identifier gives 400' 5 | level = 1 6 | category = 1 7 | versions = [u'1.0', u'1.1', u'2.0', u'3.0'] 8 | validationInfo = None 9 | 10 | def run(self, result): 11 | try: 12 | url = result.make_url({'identifier': '[frob]'}) 13 | url = url.replace('%5B', '[') 14 | url = url.replace('%5D', ']') 15 | error = result.fetch(url) 16 | self.validationInfo.check('status', result.last_status, [400, 404], result) 17 | return result 18 | except Exception as error: 19 | raise ValidatorError('url-check', str(error), 400, result, 'Failed to get random identifier from url: {}.'.format(url)) 20 | -------------------------------------------------------------------------------- /iiif_validator/tests/id_escaped.py: -------------------------------------------------------------------------------- 1 | from .test import BaseTest, ValidatorError 2 | 3 | class Test_Id_Escaped(BaseTest): 4 | label = 'Escaped characters processed' 5 | level = 1 6 | category = 1 7 | versions = [u'1.0', u'1.1', u'2.0', u'3.0'] 8 | validationInfo = None 9 | 10 | def run(self, result): 11 | try: 12 | idf = result.identifier.replace('-', '%2D') 13 | url = result.make_url({'identifier':idf}) 14 | data = result.fetch(url) 15 | self.validationInfo.check('status', result.last_status, 200, result) 16 | img = result.make_image(data) 17 | return result 18 | except Exception as error: 19 | raise ValidatorError('url-check', str(error), 404, result, 'Failed to get random identifier from url: {}.'.format(url)) 20 | -------------------------------------------------------------------------------- /iiif_validator/tests/id_squares.py: -------------------------------------------------------------------------------- 1 | from .test import BaseTest, ValidatorError 2 | import random 3 | 4 | class Test_Id_Squares(BaseTest): 5 | label = 'Correct image returned' 6 | level = 0 7 | category = 1 8 | versions = [u'1.0', u'1.1', u'2.0', u'3.0'] 9 | validationInfo = None 10 | 11 | def run(self, result): 12 | url = result.make_url({'format':'jpg'}) 13 | try: 14 | data = result.fetch(url) 15 | self.validationInfo.check('status', result.last_status, 200, result) 16 | img = result.make_image(data) 17 | # Now test some squares for correct color 18 | 19 | match = 0 20 | for i in range(5): 21 | x = random.randint(0,9) 22 | y = random.randint(0,9) 23 | xi = x * 100 + 13; 24 | yi = y * 100 + 13; 25 | box = (xi,yi,xi+74,yi+74) 26 | sqr = img.crop(box) 27 | ok = self.validationInfo.do_test_square(sqr, x, y, result) 28 | if ok: 29 | match += 1 30 | else: 31 | error = (x,y) 32 | if match >= 4: 33 | return result 34 | else: 35 | raise ValidatorError('color', 1,0, result) 36 | except: 37 | raise ValidatorError('status', result.last_status, 200, result, 'Failed to retrieve url: {}'.format(url)) 38 | -------------------------------------------------------------------------------- /iiif_validator/tests/info_json.py: -------------------------------------------------------------------------------- 1 | 2 | from .test import BaseTest, ValidatorError 3 | 4 | class Test_Info_Json(BaseTest): 5 | label = "Check Image Information" 6 | level = 0 7 | category = 1 8 | versions = ["1.0","1.1","2.0","3.0"] 9 | validationInfo = None 10 | 11 | def __init__(self, info): 12 | self.validationInfo = info 13 | 14 | def run(self, result): 15 | # Does server have info.json 16 | try: 17 | info = result.get_info() 18 | if info == None: 19 | raise ValidatorError('info.json is JSON', True, False, result) 20 | 21 | self.validationInfo.check('required-field: width', 'width' in info, True, result) 22 | self.validationInfo.check('required-field: height', 'height' in info, True, result) 23 | self.validationInfo.check('type-is-int: height', type(info['height']) == int, True, result) 24 | self.validationInfo.check('type-is-int: width', type(info['width']) == int, True, result) 25 | 26 | # Now switch on version 27 | if result.version == "1.0": 28 | self.validationInfo.check('required-field: identifier', 'identifier' in info, True, result) 29 | else: 30 | idField = '@id' 31 | if result.version[0] == '3': 32 | idField = 'id' 33 | self.validationInfo.check('required-field: {}'.format(idField), idField in info, True, result, 'info.json is missing required field `{}`'.format(idField)) 34 | self.validationInfo.check('type-is-uri: {}'.format(idField), info[idField].startswith('http'), True, result) 35 | # Check id is same as request URI 36 | self.validationInfo.check('{} is correct URI'.format(idField), info[idField] == result.last_url.replace('/info.json', ''), True, result, 'Found: {} Expected: {}'.format(info[idField], result.last_url.replace('/info.json', ''))) 37 | 38 | self.validationInfo.check('required-field: @context', '@context' in info, True, result) 39 | if result.version == "1.1": 40 | self.validationInfo.check('correct-context', info['@context'], 41 | ["http://library.stanford.edu/iiif/image-api/1.1/context.json", "http://iiif.io/api/image/1/context.json"], result) 42 | elif result.version[0] == "2": 43 | self.validationInfo.check('correct-context', info['@context'], "http://iiif.io/api/image/2/context.json", result) 44 | elif result.version[0] == "3": 45 | if type(info['@context']) == list: 46 | self.validationInfo.check('correct-context', "http://iiif.io/api/image/3/context.json" in info['@context'], True, result,'@context missing version 3.0 IIIF context: http://iiif.io/api/image/3/context.json') 47 | else: 48 | self.validationInfo.check('correct-context', info['@context'], "http://iiif.io/api/image/3/context.json", result) 49 | 50 | 51 | if int(result.version[0]) >= 2: 52 | self.validationInfo.check('required-field: protocol', 'protocol' in info, True, result) 53 | self.validationInfo.check('correct-protocol', info['protocol'], 'http://iiif.io/api/image', result) 54 | 55 | if result.version[0] == "2" or result.version[0] == "3": 56 | self.validationInfo.check('required-field: profile', 'profile' in info, True, result) 57 | profs = info['profile'] 58 | if result.version[0] == "2": 59 | self.validationInfo.check('is-list', type(profs), list, result, 'Profile should be a list.') 60 | self.validationInfo.check('profile-compliance', profs[0].startswith('http://iiif.io/api/image/2/level'), True, result) 61 | else: 62 | self.validationInfo.check('profile-compliance', profs in ['level0', 'level1', 'level2'], True, result, 'Profile should be one of level0, level1 or level2. https://iiif.io/api/image/3.0/#6-compliance-level-and-profile-document') 63 | 64 | if 'sizes' in info: 65 | sizes = info['sizes'] 66 | self.validationInfo.check('is-list', type(sizes), list, result) 67 | for sz in sizes: 68 | self.validationInfo.check('is-object', type(sz), dict, result) 69 | self.validationInfo.check('required-field: height', 'height' in sz, True, result) 70 | self.validationInfo.check('required-field: width', 'width' in sz, True, result) 71 | self.validationInfo.check('type-is-int: height', type(sz['height']), int, result) 72 | self.validationInfo.check('type-is-int: width', type(sz['width']), int, result) 73 | 74 | if 'tiles' in info: 75 | tiles = info['tiles'] 76 | self.validationInfo.check('is-list', type(tiles), list, result) 77 | for t in tiles: 78 | self.validationInfo.check('is-object', type(t), dict, result) 79 | self.validationInfo.check('required-field: scaleFactors', 'scaleFactors' in t, True, result) 80 | self.validationInfo.check('required-field: width', 'width' in t, True, result) 81 | self.validationInfo.check('type-is-int: width', type(t['width']), int, result) 82 | # extra version 3.0 checks 83 | if result.version[0] == "3": 84 | self.validationInfo.check('correct-type', 'type' in info and info['type'], "ImageService3", result, "Info.json missing required type of ImageService3.") 85 | self.validationInfo.check('license-renamed', 'license' in info, False, result,'license has been renamed rights in 3.0',warning=True) 86 | if 'rights' in info: 87 | self.validationInfo.check('type-is-uri: rights', info['rights'].startswith('http'), True, result,'Rights should be a single URI from Creative Commons, RightsStatements.org or URIs registered as extensions.') 88 | if 'extraQualities' in info: 89 | self.validationInfo.check('is-list', type(info['extraQualities']), list, result, 'extraQualities should be a list.') 90 | if 'extraFormats' in info: 91 | self.validationInfo.check('is-list', type(info['extraFormats']), list, result, 'extraFormats should be a list.') 92 | if 'extraFeatures' in info: 93 | self.validationInfo.check('is-list', type(info['extraFeatures']), list, result, 'extraFeatures should be a list.') 94 | 95 | self.checkLinkingProperties('service', info, result) 96 | self.checkLinkingProperties('partOf', info, result) 97 | self.checkLinkingProperties('seeAlso', info, result) 98 | 99 | self.validationInfo.check('attribution-missing', 'attribution' in info, False, result,'attribution has been removed in 3.0',warning=True) 100 | self.validationInfo.check('logo-missing', 'logo' in info, False, result,'logo has been removed in 3.0',warning=True) 101 | 102 | return result 103 | except Exception as exception: 104 | self.validationInfo.check('status', result.last_status, 200, result, "Failed to reach {} due to http status code: {}.".format(result.make_info_url(), result.last_status)) 105 | ct = result.last_headers['content-type'] 106 | scidx = ct.find(';') 107 | if scidx > -1: 108 | ct = ct[:scidx] 109 | self.validationInfo.check('content-type', 'application/json' in result.last_headers['content-type'] or 'application/ld+json' in result.last_headers['content-type'], True, result, 'Content-type for the info.json needs to be either application/json or application/ld+json.') 110 | raise 111 | 112 | def checkLinkingProperties(self, name, info, result): 113 | if name in info: 114 | self.validationInfo.check('is-list', type(info[name]), list, result, '{} should be a list.'.format(name)) 115 | for item in info[name]: 116 | self.validationInfo.check('is-object', type(item), dict, result, 'Item: {} in {} should be an object.'.format(item, name)) 117 | 118 | if name == 'service' and (u'id' not in item and u'@id' not in item) or (u'type' not in item and u'@type' not in item): 119 | raise ValidatorError('missing-key', '','id, @id, type or @type', result, 'Item: {} in {} needs a id and type or @id and @type'.format(item,name)) 120 | elif name != 'service' and ('id' not in item or 'type' not in item): 121 | raise ValidatorError('missing-key', '','id or type missing', result, 'Item: {} in {} needs a id and type'.format(item, name)) 122 | 123 | #if seeAlso should have label, format, profile 124 | #if partOf should have label 125 | #if service should have profile 126 | self.checkLabel(item, result) 127 | 128 | def checkLabel(self, parent, result): 129 | if 'label' in parent: 130 | self.validationInfo.check('is-object', type(parent['label']), dict, result, 'Label must be an object') 131 | for lang in parent['label']: 132 | self.validationInfo.check('is-list', type(parent['label'][lang]), list, result, 'Value of Label with lng: {} should be list'.format(lang)) 133 | -------------------------------------------------------------------------------- /iiif_validator/tests/info_xml.py: -------------------------------------------------------------------------------- 1 | from .test import BaseTest, ValidatorError 2 | from lxml import etree 3 | 4 | class Test_Info_Xml(BaseTest): 5 | label = "Check Image Information (XML)" 6 | level = 0 7 | category = 1 8 | versions = ["1.0"] 9 | validationInfo = None 10 | 11 | def __init__(self, info): 12 | self.validationInfo = info 13 | 14 | def run(self, result): 15 | url = result.make_info_url('xml') 16 | try: 17 | data = result.fetch(url) 18 | except: 19 | self.validationInfo.check('status', result.last_status, 200, result) 20 | self.validationInfo.check('format', result.last_headers['content-type'], ['application/xml', 'text/xml'], result) 21 | raise 22 | try: 23 | dom = etree.XML(data) 24 | except: 25 | raise ValidatorError('format', 'XML', 'Unknown', result) 26 | 27 | ns = { 'i':'http://library.stanford.edu/iiif/image-api/ns/'} 28 | self.validationInfo.check('required-field: /info', len(dom.xpath('/i:info', namespaces=ns)), 1, result) 29 | self.validationInfo.check('required-field: /info/identifier', len(dom.xpath('/i:info/i:identifier', namespaces=ns)), 1, result) 30 | self.validationInfo.check('required-field: /info/height', len(dom.xpath('/i:info/i:height', namespaces=ns)), 1, result) 31 | self.validationInfo.check('required-field: /info/width', len(dom.xpath('/i:info/i:width', namespaces=ns)), 1, result) 32 | return result -------------------------------------------------------------------------------- /iiif_validator/tests/jsonld.py: -------------------------------------------------------------------------------- 1 | from .test import BaseTest 2 | try: 3 | # python3 4 | from urllib.request import Request, urlopen, HTTPError 5 | except ImportError: 6 | # fall back to python2 7 | from urllib2 import Request, urlopen, HTTPError 8 | 9 | class Test_Jsonld(BaseTest): 10 | label = 'JSON-LD Media Type' 11 | level = 1 12 | category = 7 13 | versions = [u'2.0', u'3.0'] 14 | validationInfo = None 15 | 16 | def run(self, result): 17 | url = result.make_info_url() 18 | hdrs = {'Accept': 'application/ld+json'} 19 | try: 20 | r = Request(url, headers=hdrs) 21 | wh = urlopen(r) 22 | img = wh.read() 23 | wh.close() 24 | except HTTPError as e: 25 | wh = e 26 | self.validationInfo.check('status', result.last_status, 200, result) 27 | ct = wh.headers['content-type'] 28 | self.validationInfo.check('json-ld', ct.startswith('application/ld+json'), 1, result, "Content-Type to start with application/ld+json") 29 | return result 30 | -------------------------------------------------------------------------------- /iiif_validator/tests/linkheader_canonical.py: -------------------------------------------------------------------------------- 1 | from .test import BaseTest, ValidatorError 2 | 3 | class Test_Linkheader_Canonical(BaseTest): 4 | label = 'Canonical Link Header' 5 | level = 3 6 | category = 7 7 | versions = [u'2.0', u'3.0'] 8 | validationInfo = None 9 | 10 | def run(self, result): 11 | 12 | url = result.make_url(params={}) 13 | data = result.fetch(url) 14 | self.validationInfo.check('status', result.last_status, 200, result) 15 | try: 16 | lh = result.last_headers['link'] 17 | except KeyError: 18 | raise ValidatorError('canonical', '', 'URI', result, 'Missing "link" header in response.') 19 | links = result.parse_links(lh) 20 | canonical = result.get_uri_for_rel(links, 'canonical') 21 | if not canonical: 22 | raise ValidatorError('canonical', links, 'canonical link header', result, 'Found link header but not canonical.') 23 | else: 24 | result.tests.append('linkheader') 25 | return result 26 | -------------------------------------------------------------------------------- /iiif_validator/tests/linkheader_profile.py: -------------------------------------------------------------------------------- 1 | from .test import BaseTest, ValidatorError 2 | 3 | class Test_Linkheader_Profile(BaseTest): 4 | label = 'Profile Link Header' 5 | level = 3 6 | category = 7 7 | versions = [u'1.0', u'1.1', u'2.0', u'3.0'] 8 | validationInfo = None 9 | 10 | def run(self, result): 11 | url = result.make_url(params={}) 12 | data = result.fetch(url) 13 | self.validationInfo.check('status', result.last_status, 200, result) 14 | try: 15 | lh = result.last_headers['link'] 16 | except KeyError: 17 | raise ValidatorError('profile', '', 'URI', result,'Missing "link" header in response.') 18 | 19 | links = result.parse_links(lh) 20 | profile = result.get_uri_for_rel(links, 'profile') 21 | if not profile: 22 | raise ValidatorError('profile', '', 'URI', result) 23 | elif result.version == "1.0" and not profile.startswith('http://library.stanford.edu/iiif/image-api/compliance.html'): 24 | raise ValidatorError('profile', profile, 'http://library.stanford.edu/iiif/image-api/compliance.html', result, "Profile link header returned unexpected link.") 25 | elif result.version == "1.1" and not profile.startswith('http://library.stanford.edu/iiif/image-api/1.1/compliance.html'): 26 | raise ValidatorError('profile', profile, 'http://library.stanford.edu/iiif/image-api/1.1/compliance.html', result, "Profile link header returned unexpected link.") 27 | elif result.version.startswith("2") and not profile.startswith('http://iiif.io/api/image/2/'): 28 | raise ValidatorError('profile', profile, 'http://iiif.io/api/image/2/', result, "Profile link header returned unexpected link.") 29 | elif result.version.startswith("3") and not profile.startswith('http://iiif.io/api/image/3/'): 30 | raise ValidatorError('profile', profile, 'http://iiif.io/api/image/3/', result, "Profile link header returned unexpected link.") 31 | else: 32 | result.tests.append('linkheader') 33 | return result 34 | -------------------------------------------------------------------------------- /iiif_validator/tests/quality_bitonal.py: -------------------------------------------------------------------------------- 1 | from .test import BaseTest 2 | 3 | class Test_Quality_Bitonal(BaseTest): 4 | label = 'Bitonal quality' 5 | level = 2 6 | category = 5 7 | versions = [u'1.0', u'1.1', u'2.0', u'3.0'] 8 | validationInfo = None 9 | 10 | def run(self, result): 11 | try: 12 | params = {'quality': 'bitonal'} 13 | img = result.get_image(params) 14 | 15 | cols = img.getcolors() 16 | # cols should be [(x, 0), (y,255)] or [(x,(0,0,0)), (y,(255,255,255))] 17 | if img.mode == '1' or img.mode == 'L': 18 | return self.validationInfo.check('quality', 1, 1, result) 19 | else: 20 | # check vast majority of px are 0,0,0 or 255,255,255 21 | okpx = sum([x[0] for x in cols if sum(x[1]) < 15 or sum(x[1]) > 750]) 22 | if okpx > 650000: 23 | return self.validationInfo.check('quality', 1,1, result) 24 | else: 25 | return self.validationInfo.check('quality', 1,0, result) 26 | except: 27 | self.validationInfo.check('status', result.last_status, 200, result) 28 | raise 29 | -------------------------------------------------------------------------------- /iiif_validator/tests/quality_color.py: -------------------------------------------------------------------------------- 1 | from .test import BaseTest 2 | 3 | class Test_Quality_Color(BaseTest): 4 | label = 'Color quality' 5 | level = 2 6 | category = 5 7 | versions = [u'1.0', u'1.1', u'2.0', u'3.0'] 8 | validationInfo = None 9 | 10 | def run(self, result): 11 | try: 12 | params = {'quality': 'color'} 13 | img = result.get_image(params) 14 | # XXX should check we actually are getting color back 15 | self.validationInfo.check('quality', img.mode, ['RGB','P'], result) 16 | return result 17 | except: 18 | self.validationInfo.check('status', result.last_status, 200, result) 19 | raise 20 | -------------------------------------------------------------------------------- /iiif_validator/tests/quality_error_random.py: -------------------------------------------------------------------------------- 1 | from .test import BaseTest, ValidatorError 2 | 3 | class Test_Quality_Error_Random(BaseTest): 4 | label = 'Random quality gives 400' 5 | level = 1 6 | category = 5 7 | versions = [u'1.0', u'1.1', u'2.0', u'3.0'] 8 | validationInfo = None 9 | 10 | def run(self, result): 11 | try: 12 | url = result.make_url({'quality': self.validationInfo.make_randomstring(6)}) 13 | error = result.fetch(url) 14 | self.validationInfo.check('status', result.last_status, 400, result) 15 | return result 16 | except Exception as error: 17 | raise ValidatorError('url-check', str(error), 400, result, 'Failed to get random quality from url: {}.'.format(url)) 18 | -------------------------------------------------------------------------------- /iiif_validator/tests/quality_grey.py: -------------------------------------------------------------------------------- 1 | from .test import BaseTest 2 | 3 | class Test_Quality_Grey(BaseTest): 4 | label = 'Gray/Grey quality' 5 | level = 2 6 | category = 5 7 | versions = [u'1.0', u'1.1', u'2.0', u'3.0'] 8 | validationInfo = None 9 | 10 | def run(self, result): 11 | try: 12 | params = {'quality': 'grey'} 13 | img = result.get_image(params) 14 | 15 | if img.mode == 1: 16 | return self.validationInfo.check('quality', 1, 0, result) 17 | elif img.mode == 'L': 18 | return self.validationInfo.check('quality', 1, 1, result) 19 | else: 20 | cols = img.getcolors(maxcolors=1000000) #1kx1k image so <=1M colors 21 | # check vast majority of px are triples with v similar r,g,b 22 | ttl = 0 23 | for c in cols: 24 | if (abs(c[1][0] - c[1][1]) < 5 and abs(c[1][1] - c[1][2]) < 5): 25 | ttl += c[0] 26 | if ttl > 650000: 27 | return self.validationInfo.check('quality', 1,1, result) 28 | else: 29 | return self.validationInfo.check('quality', 1,0, result) 30 | 31 | return result 32 | except: 33 | self.validationInfo.check('status', result.last_status, 200, result) 34 | raise 35 | -------------------------------------------------------------------------------- /iiif_validator/tests/region_error_random.py: -------------------------------------------------------------------------------- 1 | from .test import BaseTest, ValidatorError 2 | 3 | class Test_Region_Error_Random(BaseTest): 4 | label = 'Random region gives 400' 5 | level = 1 6 | category = 2 7 | versions = [u'1.0', u'1.1', u'2.0', u'3.0'] 8 | validationInfo = None 9 | 10 | def run(self, result): 11 | try: 12 | url = result.make_url({'region': self.validationInfo.make_randomstring(6)}) 13 | error = result.fetch(url) 14 | self.validationInfo.check('status', result.last_status, 400, result) 15 | return result 16 | except Exception as error: 17 | raise ValidatorError('url-check', str(error), 404, result, 'Failed to get random region with url {}.'.format(url)) 18 | -------------------------------------------------------------------------------- /iiif_validator/tests/region_percent.py: -------------------------------------------------------------------------------- 1 | from .test import BaseTest, ValidatorError 2 | import random 3 | 4 | class Test_Region_Percent(BaseTest): 5 | label = 'Region specified by percent' 6 | level = 2 7 | category = 2 8 | versions = [u'1.0', u'1.1', u'2.0', u'3.0'] 9 | validationInfo = None 10 | 11 | def run(self, result): 12 | try: 13 | match = 0 14 | for i in range(5): 15 | x = random.randint(0,9) 16 | y = random.randint(0,9) 17 | params = {'region' : 'pct:%s,%s,9,9' % (x*10+1, y*10+1)} 18 | img = result.get_image(params) 19 | ok = self.validationInfo.do_test_square(img,x,y, result) 20 | if ok: 21 | match += 1 22 | if match >= 4: 23 | return result 24 | else: 25 | raise ValidatorError('color', 1,0, result) 26 | except: 27 | self.validationInfo.check('status', result.last_status, 200, result) 28 | raise 29 | -------------------------------------------------------------------------------- /iiif_validator/tests/region_pixels.py: -------------------------------------------------------------------------------- 1 | from .test import BaseTest, ValidatorError 2 | import random 3 | 4 | class Test_Region_Pixels(BaseTest): 5 | label = 'Region specified by pixels' 6 | level = 1 7 | category = 2 8 | versions = [u'1.0', u'1.1', u'2.0', u'3.0'] 9 | validationInfo = None 10 | 11 | def run(self, result): 12 | try: 13 | match = 0 14 | for i in range(5): 15 | x = random.randint(0,9) 16 | y = random.randint(0,9) 17 | 18 | ix = x*100+13 19 | iy = y*100+13 20 | hw = 74 21 | params = {'region' :'%s,%s,%s,%s' % (ix,iy, hw, hw)} 22 | img = result.get_image(params) 23 | try: 24 | ok = self.validationInfo.do_test_square(img,x,y, result) 25 | except TypeError as error: 26 | raise ValidatorError('color-error', str(error), 'No error', result,'Failed to check colour due to {}'.format(error)) 27 | 28 | if ok: 29 | match += 1 30 | if match >= 4: 31 | return result 32 | else: 33 | raise ValidatorError('color', 1,0, result) 34 | except Exception as error: 35 | self.validationInfo.check('status', result.last_status, 200, result) 36 | raise ValidatorError('General error', str(error), 'No error', result,'Failed to check size due to: {}'.format(error)) 37 | -------------------------------------------------------------------------------- /iiif_validator/tests/region_square.py: -------------------------------------------------------------------------------- 1 | from .test import BaseTest, ValidatorError 2 | import random 3 | 4 | class Test_Region_Square(BaseTest): 5 | label = 'Request a square region of the full image.' 6 | level = level = {u'3.0': 1, u'2.1': 3, u'2.1.1': 1} 7 | category = 3 8 | versions = [u'3.0', u'2.1', u'2.1.1'] 9 | validationInfo = None 10 | 11 | def run(self, result): 12 | params = {'region': 'square'} 13 | try: 14 | img = result.get_image(params) 15 | except: 16 | pass 17 | 18 | # should this be a warning as size extension called full could be allowed 19 | self.validationInfo.check('square-region', result.last_status, 200, result, "A square region is manditory for levels 1 and 2 in IIIF version 3.0.") 20 | self.validationInfo.check('square-region', img.size[0], img.size[1], result, "Square region returned a rectangle of unequal lenghts.") 21 | return result 22 | -------------------------------------------------------------------------------- /iiif_validator/tests/rot_error_random.py: -------------------------------------------------------------------------------- 1 | from .test import BaseTest, ValidatorError 2 | 3 | class Test_Rot_Error_Random(BaseTest): 4 | label = 'Random rotation gives 400' 5 | level = 1 6 | category = 4 7 | versions = [u'1.0', u'1.1', u'2.0', u'3.0'] 8 | validationInfo = None 9 | 10 | def run(self, result): 11 | try: 12 | url = result.make_url({'rotation': self.validationInfo.make_randomstring(4)}) 13 | error = result.fetch(url) 14 | self.validationInfo.check('status', result.last_status, 400, result) 15 | return result 16 | except Exception as error: 17 | raise ValidatorError('url-check', str(error), 404, result, 'Failed to get random rotation from url: {}.'.format(url)) 18 | -------------------------------------------------------------------------------- /iiif_validator/tests/rot_full_basic.py: -------------------------------------------------------------------------------- 1 | from .test import BaseTest, ValidatorError 2 | 3 | class Test_Rot_Full_Basic(BaseTest): 4 | label = 'Rotation by 90 degree values' 5 | level = {u'3.0': 2, u'2.0': 2, u'1.0': 1, u'1.1': 1} 6 | category = 4 7 | versions = [u'1.0', u'1.1', u'2.0', u'3.0'] 8 | validationInfo = None 9 | 10 | def run(self, result): 11 | try: 12 | params = {'rotation': '180'} 13 | img = result.get_image(params) 14 | s = 1000 15 | if not img.size[0] in [s-1, s, s+1]: 16 | raise ValidatorError('size', img.size, (s,s)) 17 | # Test 0,0 vs 9,9 18 | box = (12,12,76,76) 19 | sqr = img.crop(box) 20 | ok = self.validationInfo.do_test_square(sqr, 9, 9, result) 21 | if not ok: 22 | raise ValidatorError('color', 1, self.validationInfo.colorInfo[9][9], result) 23 | box = (912,912,976,976) 24 | sqr = img.crop(box) 25 | ok = self.validationInfo.do_test_square(sqr, 0, 0, result) 26 | if not ok: 27 | raise ValidatorError('color', 1, self.validationInfo.colorInfo[0][0], result) 28 | 29 | params = {'rotation': '90'} 30 | img = result.get_image(params) 31 | s = 1000 32 | if not img.size[0] in [s-1, s, s+1]: 33 | raise ValidatorError('size', img.size, (s,s)) 34 | # Test 0,0 vs 9,0 35 | box = (12,12,76,76) 36 | sqr = img.crop(box) 37 | ok = self.validationInfo.do_test_square(sqr, 0, 9, result) 38 | if not ok: 39 | raise ValidatorError('color', 1, self.validationInfo.colorInfo[9][9], result) 40 | box = (912,912,976,976) 41 | sqr = img.crop(box) 42 | ok = self.validationInfo.do_test_square(sqr, 9, 0, result) 43 | if not ok: 44 | raise ValidatorError('color', 1, self.validationInfo.colorInfo[0][0], result) 45 | 46 | params = {'rotation': '270'} 47 | img = result.get_image(params) 48 | s = 1000 49 | if not img.size[0] in [s-1, s, s+1]: 50 | raise ValidatorError('size', img.size, (s,s)) 51 | # Test 0,0 vs 9,0 52 | box = (12,12,76,76) 53 | sqr = img.crop(box) 54 | ok = self.validationInfo.do_test_square(sqr, 9, 0, result) 55 | if not ok: 56 | raise ValidatorError('color', 1, self.validationInfo.colorInfo[9][9], result) 57 | box = (912,912,976,976) 58 | sqr = img.crop(box) 59 | ok = self.validationInfo.do_test_square(sqr, 0, 9, result) 60 | if not ok: 61 | raise ValidatorError('color', 1, self.validationInfo.colorInfo[0][0], result) 62 | 63 | return result 64 | 65 | except: 66 | self.validationInfo.check('status', result.last_status, 200, result) 67 | raise 68 | -------------------------------------------------------------------------------- /iiif_validator/tests/rot_full_non90.py: -------------------------------------------------------------------------------- 1 | from .test import BaseTest 2 | import random 3 | 4 | class Test_Rot_Full_Non90(BaseTest): 5 | label = 'Rotation by non 90 degree values' 6 | level = 3 7 | category = 4 8 | versions = [u'1.0', u'1.1', u'2.0', u'3.0'] 9 | validationInfo = None 10 | 11 | def run(self, result): 12 | try: 13 | r = random.randint(1,359) 14 | params = {'rotation': '%s' % r} 15 | img = result.get_image(params) 16 | # not sure how to test, other than we got an image 17 | return result 18 | except: 19 | self.validationInfo.check('status', result.last_status, 200, result) 20 | raise 21 | -------------------------------------------------------------------------------- /iiif_validator/tests/rot_mirror.py: -------------------------------------------------------------------------------- 1 | from .test import BaseTest, ValidatorError 2 | 3 | 4 | class Test_Rot_Mirror(BaseTest): 5 | label = 'Mirroring' 6 | level = 3 7 | category = 4 8 | versions = [u'2.0', u'3.0'] 9 | validationInfo = None 10 | 11 | def run(self, result): 12 | try: 13 | params = {'rotation': '!0'} 14 | img = result.get_image(params) 15 | s = 1000 16 | if not img.size[0] in [s-1, s, s+1]: 17 | raise ValidatorError('size', img.size, (s,s)) 18 | 19 | #0,0 vs 9,0 20 | box = (12,12,76,76) 21 | sqr = img.crop(box) 22 | ok = self.validationInfo.do_test_square(sqr, 9, 0, result) 23 | if not ok: 24 | raise ValidatorError('mirror', 1, self.validationInfo.colorInfo[9][9], result) 25 | 26 | # 9,9 vs 0,9 27 | box = (912,912,976,976) 28 | sqr = img.crop(box) 29 | ok = self.validationInfo.do_test_square(sqr, 0, 9, result) 30 | if not ok: 31 | raise ValidatorError('mirror', 1, self.validationInfo.colorInfo[0][0], result) 32 | return result 33 | except: 34 | self.validationInfo.check('status', result.last_status, 200, result) 35 | raise 36 | -------------------------------------------------------------------------------- /iiif_validator/tests/rot_mirror_180.py: -------------------------------------------------------------------------------- 1 | from .test import BaseTest, ValidatorError 2 | 3 | class Test_Rot_Mirror_180(BaseTest): 4 | label = 'Mirroring plus 180 rotation' 5 | level = 3 6 | category = 4 7 | versions = [u'2.0', u'3.0'] 8 | validationInfo = None 9 | 10 | def run(self, result): 11 | try: 12 | params = {'rotation': '!180'} 13 | img = result.get_image(params) 14 | s = 1000 15 | if not img.size[0] in [s-1, s, s+1]: 16 | raise ValidatorError('size', img.size, (s,s)) 17 | 18 | #0,0 vs 9,9 19 | box = (12,12,76,76) 20 | sqr = img.crop(box) 21 | ok = self.validationInfo.do_test_square(sqr, 0, 9, result) 22 | if not ok: 23 | raise ValidatorError('mirror', 1, self.validationInfo.colorInfo[9][9], result) 24 | 25 | # 9,9 vs 0,0 26 | box = (912,912,976,976) 27 | sqr = img.crop(box) 28 | ok = self.validationInfo.do_test_square(sqr, 9, 0, result) 29 | if not ok: 30 | raise ValidatorError('mirror', 1, self.validationInfo.colorInfo[0][0], result) 31 | return result 32 | except: 33 | self.validationInfo.check('status', result.last_status, 200, result) 34 | raise 35 | -------------------------------------------------------------------------------- /iiif_validator/tests/rot_region_basic.py: -------------------------------------------------------------------------------- 1 | from .test import BaseTest, ValidatorError 2 | import random 3 | 4 | class Test_Rot_Region_Basic(BaseTest): 5 | label = 'Rotation of region by 90 degree values' 6 | level = {u'3.0': 2, u'2.0': 2, u'1.0': 1, u'1.1': 1} 7 | category = 4 8 | versions = [u'1.0', u'1.1', u'2.0', u'3.0'] 9 | validationInfo = None 10 | 11 | def run(self, result): 12 | try: 13 | s = 76 14 | # ask for a random region, at a random size < 100 15 | for i in range(4): 16 | x = random.randint(0,9) 17 | y = random.randint(0,9) 18 | # XXX should do non 180 19 | params = {'rotation': '180'} 20 | params['region'] = '%s,%s,%s,%s' % (x*100+13, y*100+13,s,s) 21 | img = result.get_image(params) 22 | if not img.size[0] in [s-1, s, s+1]: # allow some leeway for rotation 23 | raise ValidatorError('size', img.size, (s,s)) 24 | ok = self.validationInfo.do_test_square(img,x,y, result) 25 | if not ok: 26 | raise ValidatorError('color', 1, self.validationInfo.colorInfo[0][0], result) 27 | return result 28 | except: 29 | self.validationInfo.check('status', result.last_status, 200, result) 30 | raise 31 | -------------------------------------------------------------------------------- /iiif_validator/tests/rot_region_non90.py: -------------------------------------------------------------------------------- 1 | from .test import BaseTest 2 | import random 3 | 4 | class Test_Rot_Region_Non90(BaseTest): 5 | label = 'Rotation by non 90 degree values' 6 | level = 3 7 | category = 4 8 | versions = [u'1.0', u'1.1', u'2.0', u'3.0'] 9 | validationInfo = None 10 | 11 | def run(self, result): 12 | try: 13 | # ask for a random region, at a random size < 100 14 | for i in range(4): 15 | r = random.randint(1,359) 16 | x = random.randint(0,9) 17 | y = random.randint(0,9) 18 | params = {'rotation': '%s'%r} 19 | params['region'] = '%s,%s,100,100' % (x*100, y*100) 20 | img = result.get_image(params) 21 | # not sure how to test 22 | return result 23 | except: 24 | self.validationInfo.check('status', result.last_status, 200, result) 25 | raise 26 | -------------------------------------------------------------------------------- /iiif_validator/tests/size_bwh.py: -------------------------------------------------------------------------------- 1 | from .test import BaseTest, ValidatorError 2 | import random 3 | 4 | class Test_Size_Bwh(BaseTest): 5 | label = 'Size specified by !w,h' 6 | level = 2 7 | category = 3 8 | versions = [u'1.0', u'1.1', u'2.0', u'3.0'] 9 | validationInfo = None 10 | 11 | def run(self, result): 12 | try: 13 | w = random.randint(350,750) 14 | h = random.randint(350,750) 15 | s = min(w,h) 16 | params = {'size': '!%s,%s' % (w,h)} 17 | img = result.get_image(params) 18 | self.validationInfo.check('size', img.size, (s,s), result) 19 | 20 | match = 0 21 | sqs = int(s/1000.0 * 100) 22 | for i in range(5): 23 | x = random.randint(0,9) 24 | y = random.randint(0,9) 25 | xi = x * sqs + 13; 26 | yi = y * sqs + 13; 27 | box = (xi,yi,xi+(sqs-13),yi+(sqs-13)) 28 | sqr = img.crop(box) 29 | ok = self.validationInfo.do_test_square(sqr, x, y, result) 30 | if ok: 31 | match += 1 32 | else: 33 | error = (x,y) 34 | if match >= 3: 35 | return result 36 | else: 37 | raise ValidatorError('color', 1,0, result) 38 | 39 | except: 40 | self.validationInfo.check('status', result.last_status, 200, result) 41 | raise 42 | -------------------------------------------------------------------------------- /iiif_validator/tests/size_ch.py: -------------------------------------------------------------------------------- 1 | from .test import BaseTest, ValidatorError 2 | import random 3 | 4 | class Test_Size_Ch(BaseTest): 5 | label = 'Size specified by ,h' 6 | level = 1 7 | category = 3 8 | versions = [u'1.0', u'1.1', u'2.0', u'3.0'] 9 | validationInfo = None 10 | 11 | def run(self, result): 12 | try: 13 | s = random.randint(450,750) 14 | params = {'size': ',%s' % s} 15 | img = result.get_image(params) 16 | self.validationInfo.check('size', img.size, (s,s), result) 17 | 18 | # Find square size 19 | sqs = int(s/1000.0 * 100) 20 | match = 0 21 | 22 | for i in range(5): 23 | x = random.randint(0,9) 24 | y = random.randint(0,9) 25 | xi = x * sqs + 13; 26 | yi = y * sqs + 13; 27 | box = (xi,yi,xi+(sqs-13),yi+(sqs-13)) 28 | sqr = img.crop(box) 29 | ok = self.validationInfo.do_test_square(sqr, x, y, result) 30 | if ok: 31 | match += 1 32 | else: 33 | error = (x,y) 34 | if match >= 4: 35 | return result 36 | else: 37 | raise ValidatorError('color', 1,0, result) 38 | except Exception as error: 39 | self.validationInfo.check('status', result.last_status, 200, result) 40 | raise ValidatorError('General error', str(error), 'No error', result,'Failed to check size due to: {}'.format(error)) 41 | -------------------------------------------------------------------------------- /iiif_validator/tests/size_error_random.py: -------------------------------------------------------------------------------- 1 | from .test import BaseTest, ValidatorError 2 | 3 | class Test_Size_Error_Random(BaseTest): 4 | label = 'Random size gives 400' 5 | level = 1 6 | category = 3 7 | versions = [u'1.0', u'1.1', u'2.0', u'3.0'] 8 | validationInfo = None 9 | 10 | def run(self, result): 11 | try: 12 | url = result.make_url({'size': self.validationInfo.make_randomstring(6)}) 13 | error = result.fetch(url) 14 | self.validationInfo.check('status', result.last_status, 400, result) 15 | return result 16 | except Exception as error: 17 | raise ValidatorError('url-check', str(error), 400, result, 'Failed to get random size with url {}.'.format(url)) 18 | -------------------------------------------------------------------------------- /iiif_validator/tests/size_nofull.py: -------------------------------------------------------------------------------- 1 | from .test import BaseTest, ValidatorError 2 | import random 3 | 4 | class Test_No_Size_Up(BaseTest): 5 | label = '3.0 replace full with max' 6 | level = 0 7 | category = 3 8 | versions = [u'3.0'] 9 | validationInfo = None 10 | 11 | def run(self, result): 12 | params = {'size': 'full'} 13 | try: 14 | img = result.get_image(params) 15 | except: 16 | pass 17 | 18 | # should this be a warning as size extension called full could be allowed 19 | self.validationInfo.check('size', result.last_status != 200, True, result, "Version 3.0 has replaced the size full with max.", warning=True) 20 | return result 21 | 22 | -------------------------------------------------------------------------------- /iiif_validator/tests/size_noup.py: -------------------------------------------------------------------------------- 1 | from .test import BaseTest, ValidatorError 2 | import random 3 | 4 | class Test_No_Size_Up(BaseTest): 5 | label = 'Size greater than 100% should only work with the ^ notation' 6 | level = 1 7 | category = 3 8 | versions = [u'3.0'] 9 | validationInfo = None 10 | 11 | def run(self, result): 12 | s = random.randint(1100,2000) 13 | 14 | # testing vesrion 2.x and 1.x to make sure they aren't upscaled 15 | self.checkSize(result, '%s,%s' % (s,s)) 16 | self.checkSize(result, ',%s' % (s)) 17 | self.checkSize(result, '%s,' % (s)) 18 | self.checkSize(result, 'pct:200') 19 | self.checkSize(result, '!2000,3000') 20 | 21 | return result 22 | 23 | def checkSize(self, result, sizeStr): 24 | params = {'size': sizeStr} 25 | try: 26 | img = result.get_image(params) 27 | except: 28 | self.validationInfo.check('size-upscalling', result.last_status, 400, result, "In version 3.0 image should only be upscaled using the ^ notation.") 29 | if result.last_status == 200: 30 | raise ValidatorError('size-upscalling', result.last_status, '!200', result, 'Retrieving upscailed image succeeded but should have failed as 3.0 requires the ^ for upscalling. Size: {}'.format(sizeStr)) 31 | -------------------------------------------------------------------------------- /iiif_validator/tests/size_percent.py: -------------------------------------------------------------------------------- 1 | from .test import BaseTest, ValidatorError 2 | import random 3 | 4 | class Test_Size_Percent(BaseTest): 5 | label = 'Size specified by percent' 6 | level = {u'3.0': 2, u'2.0': 1, u'1.0': 1, u'1.1': 1} 7 | category = 3 8 | versions = [u'1.0', u'1.1', u'2.0', u'3.0'] 9 | validationInfo = None 10 | 11 | def run(self, result): 12 | try: 13 | s = random.randint(45,75) 14 | params = {'size': 'pct:%s' % s} 15 | img = result.get_image(params) 16 | self.validationInfo.check('size', img.size, (s*10,s*10), result) 17 | 18 | match = 0 19 | # Find square size 20 | sqs = s 21 | for i in range(5): 22 | x = random.randint(0,9) 23 | y = random.randint(0,9) 24 | xi = x * sqs + 13; 25 | yi = y * sqs + 13; 26 | box = (xi,yi,xi+(sqs-13),yi+(sqs-13)) 27 | sqr = img.crop(box) 28 | ok = self.validationInfo.do_test_square(sqr, x, y, result) 29 | if ok: 30 | match += 1 31 | else: 32 | error = (x,y) 33 | if match >= 4: 34 | return result 35 | else: 36 | raise ValidatorError('color', 1,0, result) 37 | 38 | except Exception as error: 39 | self.validationInfo.check('status', result.last_status, 200, result) 40 | 41 | raise ValidatorError('General error', str(error), 'No error', result,'Failed to check size due to: {}'.format(error)) 42 | -------------------------------------------------------------------------------- /iiif_validator/tests/size_region.py: -------------------------------------------------------------------------------- 1 | from .test import BaseTest, ValidatorError 2 | import random 3 | 4 | class Test_Size_Region(BaseTest): 5 | label = 'Region at specified size' 6 | level = 1 7 | category = 3 8 | versions = [u'1.0', u'1.1', u'2.0', u'3.0'] 9 | validationInfo = None 10 | 11 | def run(self, result): 12 | try: 13 | # ask for a random region, at a random size < 100 so that 14 | # it is within one color square of the test image 15 | for i in range(5): 16 | s = random.randint(35,90) 17 | x = random.randint(0,9) 18 | y = random.randint(0,9) 19 | params = {'size': '%s,%s' % (s,s)} 20 | params['region'] = '%s,%s,100,100' % (x*100, y*100) 21 | img = result.get_image(params) 22 | if img.size != (s,s): 23 | raise ValidatorError('size', img.size, (s,s), result) 24 | try: 25 | ok = self.validationInfo.do_test_square(img,x,y, result) 26 | except TypeError as error: 27 | raise ValidatorError('color-error', str(error), 'No error', result,'Failed to check colour due to {}'.format(error)) 28 | 29 | if not ok: 30 | raise ValidatorError('color', 1, self.validationInfo.colorInfo[0][0], result) 31 | return result 32 | except Exception as error: 33 | raise ValidatorError('General error', str(error), 'No error', result,'Failed to check size due to: {}'.format(error)) 34 | -------------------------------------------------------------------------------- /iiif_validator/tests/size_up.py: -------------------------------------------------------------------------------- 1 | from .test import BaseTest, ValidatorError 2 | import random 3 | 4 | class Test_Size_Up(BaseTest): 5 | label = 'Size greater than 100%' 6 | level = 3 7 | category = 3 8 | versions = [u'1.0', u'1.1', u'2.0', u'3.0'] 9 | validationInfo = None 10 | 11 | def run(self, result): 12 | s = random.randint(1100,2000) 13 | params = {'size': ',%s' % s} 14 | try: 15 | img = result.get_image(params) 16 | 17 | self.validationInfo.check('size', img.size, (s,s), result) 18 | return self.checkSquares(img, s, result) 19 | except ValidatorError: 20 | raise 21 | except: 22 | if result.version.startswith("3"): 23 | self.validationInfo.check('size', result.last_status, 400, result, "In version 3.0 image should not be upscaled unless the ^ notation is used.") 24 | else: 25 | self.validationInfo.check('status', result.last_status, 200, result, 'Failed to retrieve upscaled image.') 26 | raise 27 | 28 | # Now testing vesrion 3.0 upscalling notation 29 | self.checkSize(result, (s, s), '^%s,%s' % (s,s), 'Failed to get correct size for an image using the ^ notation') 30 | self.checkSize(result, (s, s), '^,%s' % (s), 'Failed to get correct size when asking for the height only using the ^ notation') 31 | self.checkSize(result, (s, s), '^%s,' % (s), 'Failed to get correct size when asking for the width only using the ^ notation') 32 | # needs a bit more thought as maxium may not be the same as full, should check the info.json 33 | self.checkSize(result, (1000, 1000), '^max', 'Failed to get max size while using the ^ notation') 34 | self.checkSize(result, (2000, 2000), '^pct:200', 'Failed to get correct size when asking for the 200% size image and using the ^ notation') 35 | self.checkSize(result, (500, 500), '^!2000,500', 'Failed to get correct size when trying to fit in a box !2000,500 using the ^ notation but not upscallingtrying to fit in a box !2000,500 using the ^ notation but not upscalling') 36 | self.checkSize(result, (2000, 2000), '^!2000,3000', 'Failed to get correct size when trying to fit in a box !2000,3000 using the ^notation that requires upscalling.') 37 | 38 | return result 39 | 40 | def checkSize(self, result, size, sizeStr, message): 41 | params = {'size': sizeStr} 42 | try: 43 | img = result.get_image(params) 44 | except: 45 | self.validationInfo.check('status', result.last_status, 200, result, 'Failed to retrieve upscaled image using ^ notation.') 46 | self.validationInfo.check('size', img.size, size, result, message) 47 | self.checkSquares(img, size[0], result) 48 | 49 | 50 | def checkSquares(self, img, sourceSize, result): 51 | match = 0 52 | sqs = int(sourceSize / 1000.0 * 100) 53 | for i in range(5): 54 | x = random.randint(0,9) 55 | y = random.randint(0,9) 56 | xi = x * sqs + 13; 57 | yi = y * sqs + 13; 58 | box = (xi,yi,xi+(sqs-13),yi+(sqs-13)) 59 | sqr = img.crop(box) 60 | ok = self.validationInfo.do_test_square(sqr, x, y, result) 61 | if ok: 62 | match += 1 63 | else: 64 | error = (x,y) 65 | if match >= 3: 66 | return result 67 | else: 68 | raise ValidatorError('color', 1,0, result) 69 | -------------------------------------------------------------------------------- /iiif_validator/tests/size_wc.py: -------------------------------------------------------------------------------- 1 | from .test import BaseTest, ValidatorError 2 | import random 3 | 4 | class Test_Size_Wc(BaseTest): 5 | label = 'Size specified by w,' 6 | level = 1 7 | category = 3 8 | versions = [u'1.0', u'1.1', u'2.0', u'3.0'] 9 | validationInfo = None 10 | 11 | def run(self, result): 12 | try: 13 | s = random.randint(450,750) 14 | params = {'size': '%s,' % s} 15 | img = result.get_image(params) 16 | self.validationInfo.check('status', result.last_status, 200, result) 17 | self.validationInfo.check('size', img.size, (s,s), result) 18 | 19 | # Find square size 20 | sqs = int(s/1000.0 * 100) 21 | match = 0 22 | for i in range(5): 23 | x = random.randint(0,9) 24 | y = random.randint(0,9) 25 | xi = x * sqs + 13; 26 | yi = y * sqs + 13; 27 | box = (xi,yi,xi+(sqs-13),yi+(sqs-13)) 28 | sqr = img.crop(box) 29 | ok = self.validationInfo.do_test_square(sqr, x, y, result) 30 | if ok: 31 | match += 1 32 | else: 33 | error = (x,y) 34 | if match >= 4: 35 | return result 36 | else: 37 | raise ValidatorError('color', 1,0, result) 38 | except Exception as error: 39 | raise ValidatorError('General error', str(error), 'No error', result,'Failed to check size due to: {}'.format(error)) 40 | -------------------------------------------------------------------------------- /iiif_validator/tests/size_wh.py: -------------------------------------------------------------------------------- 1 | from .test import BaseTest, ValidatorError 2 | import random 3 | 4 | class Test_Size_Wh(BaseTest): 5 | label = 'Size specified by w,h' 6 | level = {u'3.0': 1, u'2.0': 2, u'1.0': 2, u'1.1': 2} 7 | category = 3 8 | # this test checks to see if the size that doesn't require upscalling is OK. 9 | versions = [u'1.0', u'1.1', u'2.0', u'3.0'] 10 | validationInfo = None 11 | 12 | def run(self, result): 13 | try: 14 | w = random.randint(350,750) 15 | h = random.randint(350,750) 16 | params = {'size': '%s,%s' % (w,h)} 17 | img = result.get_image(params) 18 | self.validationInfo.check('size', img.size, (w,h), result) 19 | 20 | match = 0 21 | sqsw = int(w/1000.0 * 100) 22 | sqsh = int(h/1000.0 * 100) 23 | for i in range(5): 24 | x = random.randint(0,9) 25 | y = random.randint(0,9) 26 | xi = x * sqsw + 13; 27 | yi = y * sqsh + 13; 28 | box = (xi,yi,xi+(sqsw-13),yi+(sqsh-13)) 29 | sqr = img.crop(box) 30 | ok = self.validationInfo.do_test_square(sqr, x, y, result) 31 | if ok: 32 | match += 1 33 | else: 34 | error = (x,y) 35 | if match >= 4: 36 | return result 37 | else: 38 | raise ValidatorError('color', 1,0, result) 39 | 40 | except: 41 | self.validationInfo.check('status', result.last_status, 200, result) 42 | raise 43 | -------------------------------------------------------------------------------- /iiif_validator/tests/test.py: -------------------------------------------------------------------------------- 1 | """BaseTest class for tests and ValidationError exception.""" 2 | 3 | class BaseTest(object): 4 | label = "test name" 5 | level = 0 6 | category = 0 7 | versions = [] 8 | validationInfo = None 9 | 10 | def __init__(self, info): 11 | self.validationInfo = info 12 | 13 | @classmethod 14 | def make_info(cls, version): 15 | if version and not version in cls.versions: 16 | return {} 17 | data = {'label': cls.label, 'level':cls.level, 'versions': cls.versions, 'category': cls.category} 18 | if type(cls.level) == dict: 19 | # If not version, need to make a choice... make it max() 20 | if version: 21 | data['level'] = cls.level[version] 22 | else: 23 | data['level'] = max(cls.level.values()) 24 | return data 25 | 26 | 27 | # this looks like it needs refactoring, along with validationInfo.check() 28 | class ValidatorError(Exception): 29 | def __init__(self, type, got, expected, result=None, message="", isWarning=False): 30 | self.type = type 31 | self.got = got 32 | self.expected = expected 33 | self.message = message 34 | self.warning = isWarning 35 | if result != None: 36 | self.url = result.last_url 37 | self.headers = result.last_headers 38 | self.status = result.last_status 39 | else: 40 | self.url = None 41 | self.headers = None 42 | self.status = None 43 | 44 | def __str__(self): 45 | if self.message: 46 | return "Expected {} for {}; Got: {} ({})".format(self.expected, self.type, self.got, self.message) 47 | else: 48 | return "Expected {} for {}; Got: {}".format(self.expected, self.type, self.got) 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /iiif_validator/validator.py: -------------------------------------------------------------------------------- 1 | """IIIF Image API validator module.""" 2 | 3 | from functools import partial 4 | from bottle import Bottle, route, run, request, response, abort, error 5 | import inspect 6 | import json 7 | import sys 8 | import random 9 | import os 10 | try: # python2 11 | import BytesIO as io 12 | # Must check for python2 first as io exists but is wrong one 13 | except ImportError: # python3 14 | import io 15 | try: 16 | # python3 17 | from urllib.request import urlopen, Request, HTTPError 18 | from urllib.error import URLError 19 | except ImportError: 20 | # fall back to python2 21 | from urllib2 import urlopen, Request, HTTPError, URLError 22 | try: 23 | from PIL import Image, ImageDraw 24 | except: 25 | import Image, ImageDraw 26 | from .tests.test import ValidatorError 27 | from . import tests 28 | 29 | 30 | class ValidationInfo(object): 31 | def __init__(self): 32 | 33 | self.mimetypes = {'bmp' : 'image/bmp', 34 | 'gif' : 'image/gif', 35 | 'jpg': 'image/jpeg', 36 | 'pcx' : 'image/pcx', 37 | 'pdf' : 'application/pdf', 38 | 'png' : 'image/png', 39 | 'tif' : 'image/tiff', 40 | 'webp' : 'image/webp'} 41 | 42 | self.pil_formats = {'BMP' : 'image/bmp', 43 | 'GIF' : 'image/gif', 44 | 'JPEG': 'image/jpeg', 45 | 'PCX' : 'image/pcx', 46 | 'PDF' : 'application/pdf', 47 | 'PNG' : 'image/png', 48 | 'TIFF' : 'image/tiff'} 49 | 50 | self.colorInfo = [[(61, 170, 126), (61, 107, 178), (82, 85, 234), (164, 122, 110), (129, 226, 88), (91, 37, 121), (138, 128, 42), (6, 85, 234), (121, 109, 204), (65, 246, 84)], 51 | [(195, 133, 120), (171, 43, 102), (118, 45, 130), (242, 105, 171), (5, 85, 105), (113, 58, 41), (223, 69, 3), (45, 79, 140), (35, 117, 248), (121, 156, 184)], 52 | [(168, 92, 163), (28, 91, 143), (86, 41, 173), (111, 230, 29), (174, 189, 7), (18, 139, 88), (93, 168, 128), (35, 2, 14), (204, 105, 137), (18, 86, 128)], 53 | [(107, 55, 178), (251, 40, 184), (47, 36, 139), (2, 127, 170), (224, 12, 114), (133, 67, 108), (239, 174, 209), (85, 29, 156), (8, 55, 188), (240, 125, 7)], 54 | [(112, 167, 30), (166, 63, 161), (232, 227, 23), (74, 80, 135), (79, 97, 47), (145, 160, 80), (45, 160, 79), (12, 54, 215), (203, 83, 70), (78, 28, 46)], 55 | [(102, 193, 63), (225, 55, 91), (107, 194, 147), (167, 24, 95), (249, 214, 96), (167, 34, 136), (53, 254, 209), (172, 222, 21), (153, 77, 51), (137, 39, 183)], 56 | [(159, 182, 192), (128, 252, 173), (148, 162, 90), (192, 165, 115), (154, 102, 2), (107, 237, 62), (111, 236, 219), (129, 113, 172), (239, 204, 166), (60, 96, 37)], 57 | [(72, 172, 227), (119, 51, 100), (209, 85, 165), (87, 172, 159), (188, 42, 162), (99, 3, 54), (7, 42, 37), (105, 155, 100), (38, 220, 240), (98, 46, 2)], 58 | [(18, 223, 145), (189, 121, 17), (88, 3, 210), (181, 16, 43), (189, 39, 244), (123, 147, 116), (246, 148, 214), (223, 177, 199), (77, 18, 136), (235, 36, 21)], 59 | [(146, 137, 176), (84, 248, 55), (61, 144, 79), (110, 251, 49), (43, 105, 132), (165, 131, 55), (60, 23, 225), (147, 197, 226), (80, 67, 104), (161, 119, 182)]] 60 | 61 | def do_test_square(self, img, x,y, result): 62 | truth = self.colorInfo[x][y] 63 | # Similarity, not necessarily perceived 64 | cols = img.getcolors(maxcolors=1000000) #1kx1k image so <=1M colors 65 | cols.sort(reverse=True) 66 | col = cols[0][1] 67 | # If image has palette, col is int and we look up [r,g,b] 68 | pal = img.getpalette() 69 | if (pal): 70 | col = [pal[col*3],pal[col*3+1],pal[col*3+2]] 71 | ok = abs(col[0]-truth[0]) < 6 and abs(col[1]-truth[1]) < 6 and abs(col[2]-truth[2]) < 6 72 | result.tests.append("%s,%s:%s" % (x,y,ok)) 73 | return ok 74 | 75 | def make_randomstring(self, length): 76 | stuff = [] 77 | for x in range(length): 78 | stuff.append(chr(random.randint(48, 122))) 79 | val = ''.join(stuff) 80 | # prevent end-of-path-segment characters 81 | val = val.replace('?', '$') 82 | val = val.replace('#', '$') 83 | val = val.replace('/', '$') 84 | return val 85 | 86 | def check(self, typ, got, expected, result=None, errmsg="", warning=False): 87 | if type(expected) == list: 88 | if not got in expected: 89 | raise ValidatorError(typ, got, expected, result, errmsg, warning) 90 | elif got != expected: 91 | raise ValidatorError(typ, got, expected, result, errmsg, warning) 92 | if result: 93 | result.tests.append(typ) 94 | return 1 95 | 96 | 97 | class TestSuite(object): 98 | 99 | def __init__(self, info): 100 | self.validationInfo = info 101 | # Look at all modules imported as tests, find Test_* classes, 102 | # take only the first class from any module 103 | self.all_tests = {} 104 | for module_name, module in inspect.getmembers(tests, inspect.ismodule): 105 | # print('mod: ' + module_name) 106 | for name, klass in inspect.getmembers(module, inspect.isclass): 107 | # print('klass: ' + name) 108 | if (name.startswith('Test_')): 109 | self.all_tests[module_name] = klass 110 | break 111 | # print(self.all_tests) 112 | 113 | def has_test(self, test): 114 | return hasattr(tests, test) 115 | 116 | def list_tests(self, version=""): 117 | allt = {} 118 | for name, klass in self.all_tests.items(): 119 | data = klass.make_info(version) 120 | if data: 121 | allt[name] = data 122 | return allt 123 | 124 | def run_test(self, test_name, result): 125 | klass = self.all_tests[test_name] 126 | test = klass(self.validationInfo) 127 | 128 | result.test_info = test.make_info(result.version) 129 | 130 | try: 131 | return test.run(result) 132 | except ValidatorError as e: 133 | result.exception = e 134 | return result 135 | 136 | class ImageAPI(object): 137 | 138 | def __init__(self, identifier, server, prefix="", scheme="http", auth="", version="2.0", debug=True): 139 | 140 | self.iiifNS = "{http://library.stanford.edu/iiif/image-api/ns/}" 141 | self.debug = debug 142 | 143 | self.scheme = scheme 144 | self.server = server 145 | if not prefix: 146 | self.prefix = "" 147 | else: 148 | self.prefix = prefix.split('/') 149 | self.identifier = identifier 150 | self.auth = auth 151 | 152 | self.version = version 153 | 154 | self.last_headers = {} 155 | self.last_status = 0 156 | self.last_url = '' 157 | 158 | # DOUBLE duty as result object 159 | self.name = "" 160 | self.urls = [] 161 | self.tests = [] 162 | self.exception = None 163 | 164 | def parse_links(self, header): 165 | 166 | state = 'start' 167 | header = header.strip() 168 | data = [d for d in header] 169 | links = {} 170 | while data: 171 | if state == 'start': 172 | d = data.pop(0) 173 | while d.isspace(): 174 | d = data.pop(0) 175 | if d != "<": 176 | raise ValueError("Parsing Link Header: Expected < in start, got %s" % d) 177 | state = "uri" 178 | elif state == "uri": 179 | uri = [] 180 | d = data.pop(0) 181 | while d != ";": 182 | uri.append(d) 183 | d = data.pop(0) 184 | uri = ''.join(uri) 185 | uri = uri[:-1] 186 | data.insert(0, ';') 187 | # Not an error to have the same URI multiple times (I think!) 188 | if (uri not in links): 189 | links[uri] = {} 190 | state = "paramstart" 191 | elif state == 'paramstart': 192 | d = data.pop(0) 193 | while data and d.isspace(): 194 | d = data.pop(0) 195 | if d == ";": 196 | state = 'linkparam'; 197 | elif d == ',': 198 | state = 'start' 199 | else: 200 | raise ValueError("Parsing Link Header: Expected ; in paramstart, got %s" % d) 201 | return 202 | elif state == 'linkparam': 203 | d = data.pop(0) 204 | while d.isspace(): 205 | d = data.pop(0) 206 | paramType = [] 207 | while not d.isspace() and d != "=": 208 | paramType.append(d) 209 | d = data.pop(0) 210 | while d.isspace(): 211 | d = data.pop(0) 212 | if d != "=": 213 | raise ValueError("Parsing Link Header: Expected = in linkparam, got %s" % d) 214 | return 215 | state='linkvalue' 216 | pt = ''.join(paramType) 217 | if (pt not in links[uri]): 218 | links[uri][pt] = [] 219 | elif state == 'linkvalue': 220 | d = data.pop(0) 221 | while d.isspace(): 222 | d = data.pop(0) 223 | paramValue = [] 224 | if d == '"': 225 | pd = d 226 | d = data.pop(0) 227 | while d != '"' and pd != '\\': 228 | paramValue.append(d) 229 | pd = d 230 | d = data.pop(0) 231 | else: 232 | while not d.isspace() and not d in (',', ';'): 233 | paramValue.append(d) 234 | if data: 235 | d = data.pop(0) 236 | else: 237 | break 238 | if data: 239 | data.insert(0, d) 240 | state = 'paramstart' 241 | pv = ''.join(paramValue) 242 | if pt == 'rel': 243 | # rel types are case insensitive and space separated 244 | links[uri][pt].extend([y.lower() for y in pv.split(' ')]) 245 | else: 246 | if not pv in links[uri][pt]: 247 | links[uri][pt].append(pv) 248 | return links 249 | 250 | def get_uri_for_rel(self, links, rel): 251 | rel = rel.lower() 252 | for (uri, info) in links.items(): 253 | rels = info.get('rel', []) 254 | if rel in rels: 255 | return uri 256 | return None 257 | 258 | def fetch(self, url): 259 | # Make it look like a real browser request 260 | HEADERS = {"Origin": "http://iiif.io/", 261 | "Referer": "http://iiif.io/api/image/validator", 262 | "User-Agent": "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre"} 263 | req = Request(url, headers=HEADERS) 264 | 265 | try: 266 | wh = urlopen(req, timeout=5) 267 | except HTTPError as e: 268 | wh = e 269 | except Exception as error: 270 | print ('Other type of error ({}): {}'.format(type(error), error)) 271 | raise 272 | data = wh.read() 273 | # nasty side effect 274 | try: # py2 275 | self.last_headers = wh.headers.dict 276 | except: # py3 277 | self.last_headers = wh.info() 278 | self.last_status = wh.code 279 | self.last_url = url 280 | wh.close() 281 | self.urls.append(url) 282 | return(data) 283 | 284 | def make_url(self, params={}): 285 | if self.prefix and 'prefix' not in params: 286 | params['prefix'] = self.prefix 287 | if 'identifier' not in params: 288 | params['identifier'] = self.identifier 289 | if 'region' not in params: 290 | params['region'] = 'full' 291 | if 'size' not in params: 292 | if self.version == "3.0": 293 | params['size'] = 'max' 294 | else: 295 | params['size'] = 'full' 296 | if 'rotation' not in params: 297 | params['rotation'] = '0' 298 | if 'quality' not in params: 299 | if self.version in ("2.0", "3.0"): 300 | params['quality'] = 'default' 301 | else: 302 | params['quality'] = 'native' 303 | elif params['quality'] == 'grey' and self.version in ("2.0", "3.0"): 304 | # en-us in 2.0+ 305 | params['quality'] = 'gray' 306 | if 'format' not in params and self.version in ("2.0", "3.0"): 307 | # format is required in 2.0+ 308 | params['format'] = 'jpg' 309 | 310 | order = ('prefix','identifier','region','size','rotation','quality') 311 | 312 | if 'prefix' in params: 313 | params['prefix'] = '/'.join(self.prefix) 314 | url = '/'.join(params.get(p) for p in order if params.get(p) is not None) 315 | 316 | if params.get('format') is not None: 317 | url+='.%s' % params['format'] 318 | 319 | scheme = params.get('scheme', self.scheme) 320 | server = params.get('server', self.server) 321 | url = "%s://%s/%s" % (scheme, server, url) 322 | if (self.debug): 323 | print(url) 324 | return url 325 | 326 | def make_image(self, data): 327 | imgio = io.BytesIO(data) 328 | img = Image.open(imgio) 329 | return img 330 | 331 | def get_image(self, params): 332 | url = self.make_url(params) 333 | imgdata = self.fetch(url) 334 | img = self.make_image(imgdata) 335 | return img 336 | 337 | def make_info_url(self, format='json'): 338 | params = {'server':self.server, 'identifier':self.identifier, 'scheme':self.scheme} 339 | if self.prefix: 340 | parts = self.prefix[:] 341 | else: 342 | parts = [] 343 | parts.extend([self.identifier, 'info']) 344 | url = '%s.%s' % ('/'.join(parts), format) 345 | scheme = params.get('scheme', self.scheme) 346 | server = params.get('server', self.server) 347 | url = "%s://%s/%s" % (self.scheme, self.server, url) 348 | return url 349 | 350 | def get_info(self): 351 | url = self.make_info_url() 352 | try: 353 | idata = self.fetch(url) 354 | except Exception as e: 355 | # uhoh 356 | #sys.stderr.write('fetch failed ' + str(e)) 357 | return None 358 | try: 359 | info = json.loads(idata.decode('utf-8')) 360 | except Exception as e: 361 | #sys.stderr.write('json.loads failed ' + str(e)) 362 | return None 363 | return info 364 | 365 | 366 | class Validator(object): 367 | 368 | def __init__(self,debug=True): 369 | if (debug): 370 | sys.stderr.write('init on Validator\n') 371 | sys.stderr.flush() 372 | 373 | def handle_test(self, testname): 374 | 375 | version = request.query.get('version', '2.0') 376 | info = ValidationInfo() 377 | testSuite = TestSuite(info) 378 | 379 | if testname == "list_tests": 380 | all_tests = testSuite.list_tests(version) 381 | return json.dumps(all_tests) 382 | if not testSuite.has_test(testname): 383 | abort(404,"No such test: %s" % testname) 384 | 385 | server = request.query.get('server', '') 386 | server = server.strip() 387 | if server.startswith('https://'): 388 | scheme = 'https' 389 | server = server.replace('https://', '') 390 | else: 391 | scheme="http" 392 | server = server.replace('http://', '') 393 | atidx = server.find('@') 394 | if atidx > -1: 395 | auth = server[:atidx] 396 | server = server[atidx+1:] 397 | else: 398 | auth = "" 399 | if not server: 400 | abort(400, "Missing mandatory parameter: server") 401 | 402 | if server[-1] == '/': 403 | server = server[:-1] 404 | 405 | prefix = request.query.get('prefix', '') 406 | prefix = prefix.strip() 407 | if prefix: 408 | prefix = prefix.replace('%2F', '/') 409 | if prefix[-1] == '/': 410 | prefix = prefix[:-1] 411 | if prefix[0] == '/': 412 | prefix = prefix[1:] 413 | 414 | identifier = request.query.get('identifier', '') 415 | identifier = identifier.strip() 416 | if not identifier: 417 | abort(400, "Missing mandatory parameter: identifier") 418 | 419 | try: 420 | result = ImageAPI(identifier, server, prefix, scheme, auth, version) 421 | 422 | testSuite.run_test(testname, result) 423 | if result.exception: 424 | e = result.exception 425 | info = {'test' : testname, 'status': 'error', 'url':result.urls, 'got':e.got, 'expected': e.expected, 'type': e.type, 'message': e.message, 'warning': e.warning} 426 | else: 427 | info = {'test' : testname, 'status': 'success', 'url':result.urls, 'tests':result.tests} 428 | if result.test_info: 429 | info['label'] = result.test_info['label'] 430 | 431 | except Exception as e: 432 | raise 433 | info = {'test' : testname, 'status': 'internal-error', 'url':e.url, 'msg':str(e)} 434 | infojson = json.dumps(info) 435 | return infojson 436 | 437 | def dispatch_views(self): 438 | pfx = "" 439 | self.app.route("/%s" % pfx, "GET", self.handle_test) 440 | 441 | def after_request(self): 442 | """A bottle hook for json responses.""" 443 | response["content_type"] = "application/json" 444 | methods = 'GET' 445 | headers = 'Origin, Accept, Content-Type, X-Requested-With, X-CSRF-Token' 446 | # Already added by apache config 447 | response.headers['Access-Control-Allow-Origin'] = '*' 448 | response.headers['Access-Control-Allow-Methods'] = methods 449 | response.headers['Access-Control-Allow-Headers'] = headers 450 | response.headers['Allow'] = methods 451 | # Add no cache so CloudFront doesn't cache response 452 | response.headers['Cache-Control'] = 'no-cache' 453 | response.headers['Pragma'] = 'no-cache' 454 | 455 | def not_implemented(self, *args, **kwargs): 456 | """Returns not implemented status.""" 457 | abort(501) 458 | 459 | def empty_response(self, *args, **kwargs): 460 | """Empty response""" 461 | 462 | options_list = empty_response 463 | options_detail = empty_response 464 | 465 | def error(self, error, message=None): 466 | """Returns the error response.""" 467 | data = json.dumps({"error": error.status_code, "message": error.body or message}) 468 | # add content-type and CORS headers to error 469 | self.after_request() 470 | return data 471 | 472 | def get_error_handler(self): 473 | """Customized errors""" 474 | return { 475 | 500: partial(self.error, message="Internal Server Error."), 476 | 404: partial(self.error, message="Document Not Found."), 477 | 501: partial(self.error, message="Not Implemented."), 478 | 405: partial(self.error, message="Method Not Allowed."), 479 | 403: partial(self.error, message="Forbidden."), 480 | 400: self.error 481 | } 482 | 483 | def get_bottle_app(self): 484 | """Returns bottle instance""" 485 | self.app = Bottle() 486 | self.dispatch_views() 487 | self.app.hook('after_request')(self.after_request) 488 | self.app.error_handler = self.get_error_handler() 489 | return self.app 490 | 491 | 492 | def apache(): 493 | v = Validator() 494 | return v.get_bottle_app() 495 | 496 | def main(): 497 | mr = Validator() 498 | run(host='localhost', reloader=True, port=8080, app=mr.get_bottle_app()) 499 | 500 | if __name__ == "__main__": 501 | main() 502 | -------------------------------------------------------------------------------- /implementations/validator/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/image-validator/6d7f3e16871aefa451ed398d90743e3c218fe5be/implementations/validator/tests/__init__.py -------------------------------------------------------------------------------- /pypi_upload.md: -------------------------------------------------------------------------------- 1 | =============================== 2 | Updating iiif-validator on pypi 3 | =============================== 4 | 5 | iiif-validator is at 6 | 7 | Putting up a new version 8 | ------------------------ 9 | 10 | 0. Bump version number working branch in iiif_validator/_version.py and check CHANGES.md is up to date 11 | 1. Check all tests good (python setup.py test; py.test) 12 | 2. Check code is up-to-date with github version 13 | 3. Check out master and merge in working branch 14 | 4. Check all tests good (python setup.py test; py.test) 15 | 5. Check branches are as expected (git branch -a) 16 | 6. Check local build and version reported OK (python setup.py build; python setup.py install) 17 | 7. Check iiif-validator.py correctly starts server and runs tests 18 | 8. If all checks out OK, tag and push the new version to github with something like: 19 | 20 | ``` 21 | git tag -n1 22 | #...current tags 23 | git tag -a -m "IIIF Image API Validator v1.1.1" v1.1.1 24 | git push --tags 25 | 26 | python setup.py sdist upload 27 | ``` 28 | 29 | FIXME - should change to use `twine` for upload per https://pypi.org/project/twine/ 30 | 31 | 32 | 9. Then check on PyPI at 33 | 10. Finally, back on working branch start new version number by editing `iiif_validator/_version.py` and `CHANGES.md` 34 | 35 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | lxml 2 | bottle<0.13 3 | python-magic 4 | pillow 5 | mock 6 | setuptools 7 | uwsgi 8 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | import os 3 | from pathlib import Path 4 | 5 | this_directory = Path(__file__).parent 6 | if os.path.exists("version.txt"): 7 | VERSION = (this_directory / "version.txt").read_text().strip() 8 | else: 9 | VERSION = "0.0.0.dev0" 10 | 11 | REQUIREMENTS = [ 12 | "bottle>=0.12.1", 13 | "python-magic>=0.4.12", 14 | "lxml>=3.7.0", 15 | "Pillow>=6.2.2" 16 | ] 17 | 18 | # Read dev requirements from requirements.txt 19 | with open("requirements.txt") as f: 20 | DEV_REQUIREMENTS = f.read().splitlines() 21 | 22 | setup( 23 | name='iiif-validator', 24 | version=VERSION, 25 | packages=['iiif_validator', 'iiif_validator.tests'], 26 | scripts=['iiif-validator.py', 'iiif-validate.py'], 27 | classifiers=[ 28 | "Development Status :: 5 - Production/Stable", 29 | "Intended Audience :: Developers", 30 | "License :: OSI Approved :: Apache Software License", 31 | "Operating System :: OS Independent", 32 | "Programming Language :: Python", 33 | "Programming Language :: Python :: 3", 34 | "Programming Language :: Python :: 3.9", 35 | "Programming Language :: Python :: 3.10", 36 | "Programming Language :: Python :: 3.11", 37 | "Programming Language :: Python :: 3.12", 38 | "Topic :: Internet :: WWW/HTTP", 39 | "Topic :: Software Development :: Libraries :: Python Modules", 40 | "Environment :: Web Environment" 41 | ], 42 | python_requires='>=3', 43 | author='IIIF Contributors', 44 | author_email='simeon.warner@cornell.edu', 45 | description='IIIF Image API Validator', 46 | long_description=open('README').read(), 47 | long_description_content_type='text/markdown', 48 | url='https://github.com/IIIF/image-validator', 49 | install_requires=REQUIREMENTS, 50 | extras_require={ 51 | "dev": DEV_REQUIREMENTS 52 | }) 53 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IIIF/image-validator/6d7f3e16871aefa451ed398d90743e3c218fe5be/tests/__init__.py -------------------------------------------------------------------------------- /tests/json/info-2.0.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "http://iiif.io/api/image/2/context.json", 3 | "@id": "http://iiif.io/api/image/2.0/example/reference/67352ccc-d1b0-11e1-89ae-279075081939", 4 | "protocol": "http://iiif.io/api/image", 5 | "width": 1000, 6 | "height": 1000, 7 | "tiles": [ 8 | { 9 | "height": 512, 10 | "scaleFactors": [ 11 | 1 12 | ], 13 | "width": 512 14 | } 15 | ], 16 | "profile": [ 17 | "http://iiif.io/api/image/2/level1.json", 18 | { 19 | "formats": [ 20 | "jpg", 21 | "png" 22 | ], 23 | "qualities": [ 24 | "default", 25 | "color", 26 | "gray" 27 | ] 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /tests/json/info-3.0-logo.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "http://iiif.io/api/image/3/context.json", 3 | "id": "http://example.com/iiif/image/67352ccc-d1b0-11e1-89ae-279075081939", 4 | "type": "ImageService3", 5 | "profile": "level1", 6 | "protocol": "http://iiif.io/api/image", 7 | "width": 1000, 8 | "height": 1000, 9 | "logo": "http://example.org/images/logo.png", 10 | "tiles": [ 11 | { 12 | "width": 512, 13 | "height": 512, 14 | "scaleFactors": [ 1 ] 15 | } 16 | ], 17 | "extraFormats": [ 18 | "jpg", 19 | "png" 20 | ], 21 | "extraQualities": [ 22 | "default", 23 | "color", 24 | "gray" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /tests/json/info-3.0-service-badlabel.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "http://iiif.io/api/image/3/context.json", 3 | "id": "http://example.com/iiif/image/67352ccc-d1b0-11e1-89ae-279075081939", 4 | "type": "ImageService3", 5 | "profile": "level1", 6 | "protocol": "http://iiif.io/api/image", 7 | "width": 1000, 8 | "height": 1000, 9 | "tiles": [ 10 | { 11 | "width": 512, 12 | "height": 512, 13 | "scaleFactors": [ 1 ] 14 | } 15 | ], 16 | "service": [{ 17 | "@id": "http://example.com/auth/1", 18 | "profile": "http://iiif.io/api/auth/1/login", 19 | "type": "FIXME need the correct type", 20 | "label":"this label should have a lang" 21 | }], 22 | "extraFormats": [ 23 | "jpg", 24 | "png" 25 | ], 26 | "extraQualities": [ 27 | "default", 28 | "color", 29 | "gray" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /tests/json/info-3.0-service-label.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "http://iiif.io/api/image/3/context.json", 3 | "id": "http://example.com/iiif/image/67352ccc-d1b0-11e1-89ae-279075081939", 4 | "type": "ImageService3", 5 | "profile": "level1", 6 | "protocol": "http://iiif.io/api/image", 7 | "width": 1000, 8 | "height": 1000, 9 | "tiles": [ 10 | { 11 | "width": 512, 12 | "height": 512, 13 | "scaleFactors": [ 1 ] 14 | } 15 | ], 16 | "service": [{ 17 | "@id": "http://example.com/auth/1", 18 | "profile": "http://iiif.io/api/auth/1/login", 19 | "type": "FIXME need the correct type", 20 | "label": { 21 | "en": ["this label should have a lang"] 22 | } 23 | }], 24 | "extraFormats": [ 25 | "jpg", 26 | "png" 27 | ], 28 | "extraQualities": [ 29 | "default", 30 | "color", 31 | "gray" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /tests/json/info-3.0-service.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "http://iiif.io/api/image/3/context.json", 3 | "id": "http://example.com/iiif/image/67352ccc-d1b0-11e1-89ae-279075081939", 4 | "type": "ImageService3", 5 | "profile": "level1", 6 | "protocol": "http://iiif.io/api/image", 7 | "width": 1000, 8 | "height": 1000, 9 | "tiles": [ 10 | { 11 | "width": 512, 12 | "height": 512, 13 | "scaleFactors": [ 1 ] 14 | } 15 | ], 16 | "service": [{ 17 | "@id": "http://example.com/auth/1", 18 | "profile": "http://iiif.io/api/auth/1/login" 19 | }], 20 | "extraFormats": [ 21 | "jpg", 22 | "png" 23 | ], 24 | "extraQualities": [ 25 | "default", 26 | "color", 27 | "gray" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /tests/json/info-3.0.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "http://iiif.io/api/image/3/context.json", 3 | "id": "http://example.com/iiif/image/67352ccc-d1b0-11e1-89ae-279075081939", 4 | "type": "ImageService3", 5 | "profile": "level1", 6 | "protocol": "http://iiif.io/api/image", 7 | "width": 1000, 8 | "height": 1000, 9 | "tiles": [ 10 | { 11 | "width": 512, 12 | "height": 512, 13 | "scaleFactors": [ 1 ] 14 | } 15 | ], 16 | "extraFormats": [ 17 | "jpg", 18 | "png" 19 | ], 20 | "extraQualities": [ 21 | "default", 22 | "color", 23 | "gray" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /tests/test_validator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Test code for validator.""" 3 | import mock 4 | from mock import MagicMock 5 | import unittest 6 | import sys 7 | import json 8 | sys.path.insert(1,'.') 9 | 10 | from iiif_validator.validator import ValidationInfo, TestSuite, ImageAPI, Validator 11 | from iiif_validator.tests.test import ValidatorError 12 | from iiif_validator.tests.cors import Test_Cors 13 | from iiif_validator.tests.info_json import Test_Info_Json 14 | 15 | class TestAll(unittest.TestCase): 16 | """Tests.""" 17 | 18 | def test01_validation_info(self): 19 | """Check information setup.""" 20 | vi = ValidationInfo() 21 | self.assertEqual(vi.mimetypes['jpg'], 'image/jpeg') 22 | 23 | def test02_test_suite_init(self): 24 | """Test suite initalization, include loading modules.""" 25 | ts = TestSuite(info=ValidationInfo()) 26 | # Check an example test 27 | self.assertTrue(ts.all_tests['info_json']) 28 | 29 | def test03_image_api_init(self): 30 | """Image API class initialization.""" 31 | ia = ImageAPI(identifier='abc', server='http://example.org/') 32 | self.assertEqual(ia.version, '2.0') 33 | 34 | def test04_validator(self): 35 | """Validator class initialization.""" 36 | v = Validator(False) 37 | self.assertTrue(hasattr(v, 'handle_test')) 38 | 39 | def test05_cors(self): 40 | """Test suite CORS.""" 41 | m = mock.Mock() 42 | t = Test_Cors(m) 43 | 44 | r = mock.Mock() 45 | setattr(r, 'last_headers', { 46 | 'Access-Control-Allow-Origin': ':-)' 47 | }) 48 | t.run(r) 49 | m.check.assert_called_with('CORS', ':-)', '*', r, 'Failed to get correct CORS header.') 50 | 51 | def test06_info(self): 52 | """Test info.json checks.""" 53 | vi = ValidationInfo() 54 | t = Test_Info_Json(vi) 55 | 56 | result = createResult('tests/json/info-3.0.json', '3.0') 57 | try: 58 | t.run(result) 59 | except: 60 | self.fail('Validator failed a valid 3.0 info.json') 61 | 62 | def test07_info3of2(self): 63 | """Test info.json checks.""" 64 | t = Test_Info_Json(ValidationInfo()) 65 | 66 | result = createResult('tests/json/info-2.0.json', '3.0') 67 | try: 68 | t.run(result) 69 | except ValidatorError as e: 70 | self.assertEqual('required-field: id', e.type,'Expected 2.0 image server to fail 3.0 validation with a missing id.') 71 | 72 | def test08_testLogoWarning(self): 73 | """Test info.json checks.""" 74 | t = Test_Info_Json(ValidationInfo()) 75 | 76 | result = createResult('tests/json/info-3.0-logo.json', '3.0') 77 | try: 78 | t.run(result) 79 | except ValidatorError as e: 80 | self.assertEqual('logo-missing', e.type,'Should have picked up logo is invalid with 3.0') 81 | self.assertTrue(e.warning,'Should be a warning not an error') 82 | 83 | def test09_testExistingAuth(self): 84 | """Test info.json checks.""" 85 | t = Test_Info_Json(ValidationInfo()) 86 | 87 | result = createResult('tests/json/info-3.0-service.json', '3.0') 88 | try: 89 | t.run(result) 90 | except ValidatorError as e: 91 | self.assertEqual('missing-key', e.type,'Expected missing type in service but found: {}'.format(e.type)) 92 | 93 | def test10_testLabel(self): 94 | """Test info.json checks.""" 95 | t = Test_Info_Json(ValidationInfo()) 96 | 97 | result = createResult('tests/json/info-3.0-service-label.json', '3.0') 98 | try: 99 | t.run(result) 100 | except ValidatorError as e: 101 | self.fail('Validator failed a valid 3.0 label: \n{}'.format(e)) 102 | 103 | def test11_testLabel(self): 104 | """Test info.json checks.""" 105 | t = Test_Info_Json(ValidationInfo()) 106 | 107 | result = createResult('tests/json/info-3.0-service-badlabel.json', '3.0') 108 | try: 109 | t.run(result) 110 | except ValidatorError as e: 111 | self.assertEqual('is-object', e.type,'Expected object test failure but got: {}'.format(e.type)) 112 | 113 | def test12_info2(self): 114 | """Test info.json checks.""" 115 | t = Test_Info_Json(ValidationInfo()) 116 | 117 | result = createResult('tests/json/info-2.0.json', '2.0') 118 | try: 119 | t.run(result) 120 | except ValidatorError as e: 121 | self.assertEqual('required-field: id', e.type,'Expected 2.0 image server to fail 3.0 validation with a missing id.') 122 | 123 | 124 | 125 | def createResult(filename, version): 126 | # result.version 127 | # result.get_info() 128 | # result.last_url 129 | # result.last_headers['content-type'] 130 | 131 | with open(filename) as f: 132 | info = json.load(f) 133 | 134 | result = mock.Mock() 135 | setattr(result, 'version', version) 136 | result.get_info = MagicMock(return_value=info) 137 | idField = 'id' 138 | if idField not in info: 139 | idField = '@id' 140 | setattr(result, 'last_url', '{}/info.json'.format(info[idField])) 141 | setattr(result, 'last_status', 200) 142 | setattr(result, 'last_headers', { 143 | 'content-type': 'application/ld+json' 144 | }) 145 | 146 | return result 147 | 148 | 149 | if __name__ == '__main__': 150 | unittest.main() 151 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | skipdist = True 3 | 4 | envlist = 5 | {py27}-{min,pypi,dev} 6 | {py34}-{min,pypi,dev} 7 | {py35}-{min,pypi,dev} 8 | {py36}-{min,pypi,dev} 9 | 10 | [testenv] 11 | setenv = 12 | PYTHONPATH = {toxinidir}:{toxinidir} 13 | 14 | deps = 15 | Requirements-Builder 16 | dev: Cython>=0.20 17 | 18 | commands = 19 | min: requirements-builder -l min -o {toxworkdir}/requirements-min.txt setup.py 20 | min: pip install -r {toxworkdir}/requirements-min.txt 21 | pypi: requirements-builder -l pypi -o {toxworkdir}/requirements-pypi.txt setup.py 22 | pypi: pip install -r {toxworkdir}/requirements-pypi.txt 23 | dev: requirements-builder -l dev -r requirements-devel.txt -o {toxworkdir}/requirements-dev.txt setup.py 24 | dev: pip install -r {toxworkdir}/requirements-dev.txt 25 | pip install -e . 26 | {envpython} setup.py test 27 | --------------------------------------------------------------------------------