├── fixtures ├── sample256x256.ico ├── sample420x240.bmp ├── sample420x240.gif ├── sample420x240.jpg ├── sample420x240.png ├── sample420x240.psd ├── sample600x450.jpg ├── sample420x240.tiff ├── sample420x240.webp ├── sample128x68-fried.png ├── sample420x240-extended.webp └── sample420x240-lossless.webp ├── .travis.yml ├── .gitignore ├── .github └── workflows │ └── test.yml ├── readme.rst ├── license ├── setup.py ├── test_imgspy.py └── imgspy.py /fixtures/sample256x256.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nkanaev/imgspy/HEAD/fixtures/sample256x256.ico -------------------------------------------------------------------------------- /fixtures/sample420x240.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nkanaev/imgspy/HEAD/fixtures/sample420x240.bmp -------------------------------------------------------------------------------- /fixtures/sample420x240.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nkanaev/imgspy/HEAD/fixtures/sample420x240.gif -------------------------------------------------------------------------------- /fixtures/sample420x240.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nkanaev/imgspy/HEAD/fixtures/sample420x240.jpg -------------------------------------------------------------------------------- /fixtures/sample420x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nkanaev/imgspy/HEAD/fixtures/sample420x240.png -------------------------------------------------------------------------------- /fixtures/sample420x240.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nkanaev/imgspy/HEAD/fixtures/sample420x240.psd -------------------------------------------------------------------------------- /fixtures/sample600x450.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nkanaev/imgspy/HEAD/fixtures/sample600x450.jpg -------------------------------------------------------------------------------- /fixtures/sample420x240.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nkanaev/imgspy/HEAD/fixtures/sample420x240.tiff -------------------------------------------------------------------------------- /fixtures/sample420x240.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nkanaev/imgspy/HEAD/fixtures/sample420x240.webp -------------------------------------------------------------------------------- /fixtures/sample128x68-fried.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nkanaev/imgspy/HEAD/fixtures/sample128x68-fried.png -------------------------------------------------------------------------------- /fixtures/sample420x240-extended.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nkanaev/imgspy/HEAD/fixtures/sample420x240-extended.webp -------------------------------------------------------------------------------- /fixtures/sample420x240-lossless.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nkanaev/imgspy/HEAD/fixtures/sample420x240-lossless.webp -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | language: python 3 | python: ['3.4', '3.5', '3.6', '3.7'] 4 | script: python3 setup.py test 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | .eggs/ 3 | *.py[cod] 4 | *$py.class 5 | build/ 6 | *.egg-info/ 7 | *.egg 8 | dist/ 9 | .coverage 10 | htmlcov/ 11 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: [push] 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | strategy: 7 | matrix: 8 | python-version: [3.4, 3.5, 3.6, 3.7, 3.8] 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Set up Python ${{ matrix.python-version }} 12 | uses: actions/setup-python@v2 13 | with: 14 | python-version: ${{ matrix.python-version }} 15 | - name: Install dependencies 16 | run: pip install pytest 17 | - name: Test 18 | run: pytest 19 | -------------------------------------------------------------------------------- /readme.rst: -------------------------------------------------------------------------------- 1 | imgspy 2 | ====== 3 | 4 | imgspy finds the metadata (type, size) of an image given its url by fetching as little as needed. This is a python implementation of `fastimage`_. Supports image types BMP, CUR, GIF, ICO, JPEG, PNG, PSD, TIFF, WEBP. 5 | 6 | .. _fastimage: https://github.com/sdsykes/fastimage 7 | 8 | usage 9 | ----- 10 | 11 | :: 12 | 13 | >>> imgspy.info('http://via.placeholder.com/1920x1080') 14 | {'type': 'png', 'width': 1920, 'height': 1080} 15 | >>> with requests.get('http://via.placeholder.com/1920x1080', stream=True) as res: 16 | ... imgspy.info(res.raw) 17 | {'type': 'png', 'width': 1920, 'height': 1080} 18 | >>> imgspy.info('/path/to/image.jpg') 19 | {'type': 'jpg', 'width': 420, 'height': 240} 20 | >>> with open('/path/to/image.jpg') as f: 21 | ... imgspy.info(f) 22 | {'type': 'jpg', 'width': 420, 'height': 240} 23 | 24 | .. image:: https://github.com/nkanaev/imgspy/workflows/test/badge.svg 25 | :target: https://github.com/nkanaev/imgspy/actions 26 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Nazar Kanaev (nkanaev@live.com) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from setuptools import setup 3 | from setuptools.command.test import test as TestCommand 4 | 5 | import imgspy 6 | 7 | 8 | class PyTestCommand(TestCommand): 9 | user_options = [("pytest-args=", "a", "Arguments to pass to pytest")] 10 | 11 | def run_tests(self): 12 | import sys, pytest 13 | sys.exit(pytest.main([])) 14 | 15 | 16 | setup( 17 | name='imgspy', 18 | version=imgspy.__version__, 19 | description='Find the size or type of the image without ' 20 | 'fetching the whole content.', 21 | long_description=imgspy.__doc__, 22 | url='https://github.com/nkanaev/imgspy', 23 | license='MIT', 24 | keywords='fastimage image size', 25 | classifiers=[ 26 | 'Development Status :: 4 - Beta', 27 | 'Intended Audience :: Developers', 28 | 'License :: OSI Approved :: MIT License', 29 | 'Operating System :: OS Independent', 30 | 'Programming Language :: Python', 31 | 'Programming Language :: Python :: 2.7', 32 | 'Programming Language :: Python :: 3.6', 33 | 'Topic :: Software Development :: Libraries :: Python Modules', 34 | ], 35 | author='Nazar Kanaev', 36 | author_email='nkanaev@live.com', 37 | py_modules=['imgspy'], 38 | test_suite='tests', 39 | tests_require=['pytest'], 40 | cmdclass={"test": PyTestCommand}, 41 | ) 42 | -------------------------------------------------------------------------------- /test_imgspy.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import glob 4 | import textwrap 5 | import urllib.request 6 | 7 | import imgspy 8 | 9 | 10 | BASEDIR = os.path.dirname(os.path.abspath(__file__)) 11 | FORMAT = r'sample(?P\d+)x(?P\d+)(?P[^.]*).(?P\w+)' 12 | 13 | 14 | def test_samples(): 15 | for filepath in glob.glob(os.path.join(BASEDIR, 'fixtures/sample*')): 16 | filename = os.path.basename(filepath) 17 | match = re.match(FORMAT, filename) 18 | expected = { 19 | 'type': match.group('format'), 20 | 'width': int(match.group('width')), 21 | 'height': int(match.group('height'))} 22 | 23 | actual = imgspy.info(open(filepath, 'rb')) 24 | assert isinstance(actual, dict), filename 25 | 26 | actual_subset = {k: v for k, v in actual.items() if k in expected} 27 | assert actual_subset == expected, filename 28 | 29 | 30 | def test_datastr(): 31 | data = textwrap.dedent('''data:image/png;base64, 32 | iVBORw0KGgoAAAANSUhEUgAAAAIAAAABCAYAAAD0In+ 33 | KAAAAD0lEQVR42mNk+M9QzwAEAAmGAYCF+yOnAAAAAElFTkSuQmCC''') 34 | assert imgspy.info(data) == {'type': 'png', 'width': 2, 'height': 1} 35 | 36 | 37 | def test_url(): 38 | domain = 'http://via.placeholder.com' 39 | urls = { 40 | domain + '/500x500.png': {'type': 'png', 'width': 500, 'height': 500}, 41 | domain + '/500x500.jpg': {'type': 'jpg', 'width': 500, 'height': 500},} 42 | for url, expected in urls.items(): 43 | assert imgspy.info(urllib.request.urlopen(url)) == expected 44 | -------------------------------------------------------------------------------- /imgspy.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ 3 | imgspy 4 | ====== 5 | 6 | imgspy finds the metadata (type, size) of an image given its url by fetching 7 | as little as needed. This is a python implementation of `fastimage`_. Supports 8 | image types BMP, CUR, GIF, ICO, JPEG, PNG, PSD, TIFF, WEBP. 9 | 10 | .. _fastimage: https://github.com/sdsykes/fastimage 11 | 12 | usage 13 | ----- 14 | 15 | :: 16 | 17 | >>> imgspy.info('http://via.placeholder.com/1920x1080') 18 | {'type': 'png', 'width': 1920, 'height': 1080} 19 | >>> with requests.get('http://via.placeholder.com/1920x1080', stream=True) as res: 20 | ... imgspy.info(res.raw) 21 | {'type': 'png', 'width': 1920, 'height': 1080} 22 | >>> imgspy.info('/path/to/image.jpg') 23 | {'type': 'jpg', 'width': 420, 'height': 240} 24 | >>> with open('/path/to/image.jpg') as f: 25 | ... imgspy.info(f) 26 | {'type': 'jpg', 'width': 420, 'height': 240} 27 | """ 28 | import io 29 | import os 30 | import sys 31 | import base64 32 | import struct 33 | import contextlib 34 | 35 | 36 | __version__ = '0.2.2' 37 | 38 | 39 | PY2 = sys.version_info[0] == 2 40 | 41 | if PY2: 42 | from urllib import urlopen 43 | else: 44 | from urllib.request import urlopen 45 | 46 | 47 | @contextlib.contextmanager 48 | def openstream(input): 49 | if hasattr(input, 'read'): 50 | yield input 51 | elif os.path.isfile(input): 52 | with open(input, 'rb') as f: 53 | yield f 54 | elif input.startswith('http'): 55 | with contextlib.closing(urlopen(input)) as f: 56 | yield f 57 | elif isinstance(input, str) and input.startswith('data:'): 58 | parts = input.split(';', 2) 59 | if len(parts) == 2 and parts[1].startswith('base64,'): 60 | yield io.BytesIO(base64.b64decode(parts[1][7:])) 61 | 62 | 63 | def info(input): 64 | with openstream(input) as stream: 65 | return probe(stream) 66 | 67 | 68 | def probe(stream): 69 | w, h = None, None 70 | chunk = stream.read(26) 71 | 72 | if chunk.startswith(b'\x89PNG\r\n\x1a\n'): 73 | if chunk[12:16] == b'IHDR': 74 | w, h = struct.unpack(">LL", chunk[16:24]) 75 | elif chunk[12:16] == b'CgBI': 76 | # fried png http://www.jongware.com/pngdefry.html 77 | chunk += stream.read(40 - len(chunk)) 78 | w, h = struct.unpack('>LL', chunk[32:40]) 79 | else: 80 | w, h = struct.unpack(">LL", chunk[8:16]) 81 | return {'type': 'png', 'width': w, 'height': h} 82 | elif chunk.startswith(b'GIF89a') or chunk.startswith(b'GIF87a'): 83 | w, h = struct.unpack('HH', data[start+5:start+9]) 93 | return {'type': 'jpg', 'width': w, 'height': h} 94 | segment_size, = struct.unpack('>H', data[start+2:start+4]) 95 | data += stream.read(segment_size + 9) 96 | start = start + segment_size + 2 97 | elif chunk.startswith(b'\x00\x00\x01\x00') or chunk.startswith(b'\x00\x00\x02\x00'): 98 | img_type = 'ico' if chunk[2:3] == b'\x01' else 'cur' 99 | num_images = struct.unpack('= 40: 109 | w, h = struct.unpack("= 5: 138 | w, h = h, w 139 | return {'type': 'tiff', 'width': w, 'height': h, 'orientation': orientation} 140 | elif chunk[:4] == b'RIFF' and chunk[8:15] == b'WEBPVP8': 141 | w, h = None, None 142 | type = chunk[15:16] 143 | chunk += stream.read(30 - len(chunk)) 144 | if type == b' ': 145 | w, h = struct.unpack('> 6)) 150 | elif type == b'X': 151 | w = 1 + struct.unpack('LL', chunk[14:22]) 156 | return {'type': 'psd', 'width': w, 'height': h} 157 | 158 | --------------------------------------------------------------------------------