Skip to content

Commit e7ad136

Browse files
committed
Initial commit; all functionality in place, tested on Ubuntu 18.04
This commit constitutes release 0.1.0.
0 parents  commit e7ad136

14 files changed

+1088
-0
lines changed

.pylintrc

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[FORMAT]
2+
3+
# Maximum number of characters on a single line.
4+
max-line-length=120
5+
6+
# Regexp for a line that is allowed to be longer than the limit.
7+
ignore-long-lines=\".*\"
8+
9+
# Allow certain single-character variable names
10+
good-names=f,w,h,c,i,j,k,x,y,z,r,g,b,e,assertRaisesRegex,assertRaisesRegexp
11+
12+
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
13+
expected-line-ending-format=LF
14+
15+
# Disable certain warnings that are not really applicable
16+
disable=too-many-instance-attributes,too-few-public-methods

.travis.yml

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
os: linux
2+
dist: xenial
3+
language: python
4+
python:
5+
- "3.6"
6+
- "3.7"
7+
install:
8+
- pip install -r requirements.txt
9+
script:
10+
- python setup.py test

LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2019 Tomi Aarnio
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

MANIFEST.in

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
include LICENSE
2+
include README.md
3+
include requirements.txt

README.md

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# imsize
2+
3+
[![Build Status](https://travis-ci.org/toaarnio/imsize.svg?branch=master)](https://travis-ci.org/toaarnio/imsize)
4+
5+
Lightning-fast extraction of image dimensions & bit depth. Tested on Python 3.6+.
6+
7+
Supports PGM / PPM / PNM / PNG / JPG / TIFF.
8+
9+
**Installing on Linux:**
10+
```
11+
pip install imsize
12+
```
13+
14+
**Building & installing from source:**
15+
```
16+
git clean -dfx
17+
python setup.py bdist_wheel
18+
pip uninstall imsize
19+
pip install --user dist/*.whl
20+
```
21+
22+
**Releasing to PyPI:**
23+
```
24+
pip install --user --upgrade setuptools wheel twine
25+
python setup.py sdist bdist_wheel
26+
twine upload dist/*
27+
```

imsize/__init__.py

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
"""
2+
Extracts image dimensions & bit depth. Supports PGM/PPM/PNM/JPG/TIFF.
3+
4+
Example:
5+
info = imsize.read("myfile.jpg")
6+
factor = info.nbytes / info.filesize
7+
print(f"{info.filespec}: compression factor = {factor.1f}")
8+
9+
https://github.com/toaarnio/imsize
10+
"""
11+
12+
from .imsize import *
13+
14+
__version__ = "0.1.0"
15+
__all__ = ["read", "ImageInfo"]

imsize/imsize

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#!/usr/bin/python3 -B
2+
3+
"""
4+
Parses a lowest common denominator set of metadata from the given
5+
PNG/PNM/JPEG/TIFF image, i.e., the dimensions and bit depth.
6+
"""
7+
8+
import os # built-in library
9+
import sys # built-in library
10+
import glob # built-in library
11+
import tqdm # pip install tqdm
12+
import imsize # pip install imsize
13+
14+
15+
if __name__ == "__main__":
16+
17+
filetypes = ["*.png", "*.pnm", "*.pgm", "*.ppm", "*.jpeg", "*.jpg", "*.tiff", "*.tif"]
18+
19+
allfiles = []
20+
21+
if len(sys.argv[1:]) == 0:
22+
for filetype in filetypes:
23+
allfiles += glob.glob(filetype)
24+
allfiles += glob.glob(filetype.upper())
25+
26+
for path in sys.argv[1:]:
27+
if os.path.isdir(path):
28+
for filetype in filetypes:
29+
allfiles += glob.glob(os.path.join(path, filetype))
30+
allfiles += glob.glob(os.path.join(path, filetype.upper()))
31+
elif os.path.isfile(path):
32+
allfiles += [path]
33+
34+
total_compressed = 0
35+
total_uncompressed = 0
36+
processed_files = []
37+
iterator = tqdm.tqdm if len(allfiles) > 100 else lambda lst: lst
38+
39+
for filespec in iterator(allfiles):
40+
info = imsize.read(filespec)
41+
if info is not None:
42+
processed_files += [filespec]
43+
total_uncompressed += info.nbytes / 1024**2
44+
total_compressed += info.filesize / 1024**2
45+
46+
print(f"Scanned {len(processed_files)} images, total {total_compressed:.1f} MB compressed, {total_uncompressed:.1f} MB uncompressed")

imsize/imsize.py

+167
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
#!/usr/bin/python3 -B
2+
3+
import os # built-in library
4+
import numpy as np # pip install numpy
5+
import piexif # pip install piexif
6+
7+
try:
8+
# package mode
9+
from imsize import pnghdr # local import: pnghdr.py
10+
from imsize import jpeghdr # local import: jpeghdr.py
11+
from imsize import pnm # local import: pnm.py
12+
except ImportError:
13+
# stand-alone mode
14+
import pnghdr
15+
import jpeghdr
16+
import pnm
17+
18+
19+
######################################################################################
20+
#
21+
# P U B L I C A P I
22+
#
23+
######################################################################################
24+
25+
26+
class ImageInfo:
27+
"""
28+
A container for image metadata, filled in and returned by read().
29+
30+
Attributes:
31+
filespec (str): The filespec given to read(), copied verbatim
32+
filetype (str): File type: "png", "pnm", "jpeg" or "exif"
33+
filesize (int): Size of the file on disk in bytes
34+
width (int): Width of the image in pixels (orientation ignored)
35+
height (int): Height of the image in pixels (orientation ignored)
36+
nchan (int): Number of color channels: 1, 2, 3 or 4
37+
bitdepth (int): Bits per sample: 1 to 16
38+
bytedepth (int): Bytes per sample: 1 or 2
39+
maxval (int): Maximum representable sample value, e.g., 255
40+
dtype (type): NumPy dtype for sample values: uint8 or uint16
41+
nbytes (int): Size of the image in bytes, uncompressed
42+
"""
43+
def __init__(self):
44+
self.filespec = None
45+
self.filetype = None
46+
self.filesize = None
47+
self.width = None
48+
self.height = None
49+
self.nchan = None
50+
self.bitdepth = None
51+
self.bytedepth = None
52+
self.maxval = None
53+
self.dtype = None
54+
self.nbytes = None
55+
56+
57+
def read(filespec):
58+
"""
59+
Parses a lowest common denominator set of metadata from the given
60+
PNG/PNM/JPEG/TIFF image, i.e., the dimensions and bit depth. Does
61+
not read the entire file but only what's necessary. Returns an
62+
ImageInfo with all fields filled in, or None in case of failure.
63+
64+
Example:
65+
info = imsize.read("myfile.jpg")
66+
factor = info.nbytes / info.filesize
67+
print(f"{info.filespec}: compression factor = {factor.1f}")
68+
"""
69+
filename = os.path.basename(filespec) # "path/image.ext" => "image.ext"
70+
extension = os.path.splitext(filename)[-1] # "image.ext" => ("image", ".ext")
71+
filetype = extension.lower()[1:] # ".EXT" => "ext"
72+
handlers = {"png": _read_png,
73+
"pnm": _read_pnm,
74+
"pgm": _read_pnm,
75+
"ppm": _read_pnm,
76+
"jpeg": _read_jpeg,
77+
"jpg": _read_jpeg,
78+
"tiff": _read_exif,
79+
"tif": _read_exif,
80+
"webp": _read_exif}
81+
if filetype in handlers:
82+
handler = handlers[filetype]
83+
info = handler(filespec)
84+
return info
85+
return None
86+
87+
88+
######################################################################################
89+
#
90+
# I N T E R N A L F U N C T I O N S
91+
#
92+
######################################################################################
93+
94+
95+
def _read_png(filespec):
96+
header = pnghdr.Png.from_file(filespec)
97+
colortype = pnghdr.Png.ColorType
98+
nchannels = {colortype.greyscale: 1,
99+
colortype.truecolor: 3,
100+
colortype.indexed: 3,
101+
colortype.greyscale_alpha: 2,
102+
colortype.truecolor_alpha: 4}
103+
info = ImageInfo()
104+
info.filespec = filespec
105+
info.filetype = "png"
106+
info.width = header.ihdr.width
107+
info.height = header.ihdr.height
108+
info.nchan = nchannels[header.ihdr.color_type]
109+
info.bitdepth = header.ihdr.bit_depth
110+
info = _complete(info)
111+
return info
112+
113+
114+
def _read_pnm(filespec):
115+
shape, maxval = pnm.dims(filespec)
116+
info = ImageInfo()
117+
info.filespec = filespec
118+
info.filetype = "pnm"
119+
info.width = shape[1]
120+
info.height = shape[0]
121+
info.nchan = shape[2]
122+
info.maxval = maxval
123+
info = _complete(info)
124+
return info
125+
126+
127+
def _read_exif(filespec):
128+
try:
129+
exif = piexif.load(filespec).pop("0th")
130+
info = ImageInfo()
131+
info.filespec = filespec
132+
info.filetype = "exif"
133+
info.width = exif.get(piexif.ImageIFD.ImageWidth)
134+
info.height = exif.get(piexif.ImageIFD.ImageLength)
135+
info.nchan = exif.get(piexif.ImageIFD.SamplesPerPixel)
136+
info.bitdepth = exif.get(piexif.ImageIFD.BitsPerSample)[0]
137+
info = _complete(info)
138+
return info
139+
except TypeError:
140+
print(f"Unable to parse {filespec}: missing/broken EXIF metadata.")
141+
return None
142+
143+
144+
def _read_jpeg(filespec):
145+
info = ImageInfo()
146+
info.filespec = filespec
147+
info.filetype = "jpeg"
148+
data = jpeghdr.Jpeg.from_file(filespec)
149+
for seg in data.segments:
150+
if seg.marker == seg.MarkerEnum.sof0:
151+
info.width = seg.data.image_width
152+
info.height = seg.data.image_height
153+
info.nchan = seg.data.num_components
154+
info.bitdepth = seg.data.bits_per_sample
155+
info = _complete(info)
156+
break
157+
return info
158+
159+
160+
def _complete(info):
161+
info.filesize = os.path.getsize(info.filespec)
162+
info.maxval = info.maxval or 2 ** info.bitdepth - 1
163+
info.bitdepth = info.bitdepth or int(np.log2(info.maxval + 1))
164+
info.bytedepth = 2 if info.maxval > 255 else 1
165+
info.dtype = np.uint16 if info.maxval > 255 else np.uint8
166+
info.nbytes = info.width * info.height * info.nchan * info.bytedepth
167+
return info

0 commit comments

Comments
 (0)