24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/__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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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_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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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_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/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/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/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/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_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_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_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_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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 }}"
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | IIIF Image API Validator
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
71 |
72 |
73 |
74 |
--------------------------------------------------------------------------------
/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/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-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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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('